Large file — syntax highlighting disabled.
1 {"file_contents":{"drizzle.config.ts":{"content":"import { defineConfig } from \"drizzle-kit\";\n\nif (!process.env.DATABASE_URL) {\n throw new Error(\"DATABASE_URL, ensure the database is provisioned\");\n}\n\nexport default defineConfig({\n out: \"./migrations\",\n schema: \"./shared/schema.ts\",\n dialect: \"postgresql\",\n dbCredentials: {\n url: process.env.DATABASE_URL,\n },\n});\n","size_bytes":325},"postcss.config.js":{"content":"export default {\n plugins: {\n tailwindcss: {},\n autoprefixer: {},\n },\n}\n","size_bytes":80},"replit.md":{"content":"# Overview\n\nJCR Guide Pro v1.0.0 is a comprehensive web application for computer repair technicians and DIY enthusiasts. It provides interactive repair guides, troubleshooting workflows, and parts identification tools for laptops and desktop computers. The platform features step-by-step repair instructions with visual aids, difficulty ratings, time estimates, and safety warnings to help users safely and effectively repair their devices.\n\n## Version 1.0.0 Release\nReleased on August 18, 2025, this major release includes full PWA support, comprehensive tracking system, advanced offline capabilities, and complete authentication integration.\n\n# User Preferences\n\nPreferred communication style: Simple, everyday language.\nUser requested dark mode implementation on August 18, 2025.\nSupport contact email: JCRguideproofficial@gmail.com for user assistance and feedback.\nAdministrator account: jhustle44@gmail.com has admin privileges with access to dashboard and user management.\n\n# System Architecture\n\n## Frontend Architecture\n- **Framework**: React with TypeScript using Vite as the build tool\n- **Routing**: Wouter for client-side routing with pages for home, repair guides, and troubleshooting\n- **UI Components**: Radix UI primitives with shadcn/ui component library for consistent design\n- **Styling**: Tailwind CSS with custom design tokens and Material Icons\n- **State Management**: TanStack Query for server state management and caching\n- **Form Handling**: React Hook Form with Zod validation\n\n## Backend Architecture\n- **Runtime**: Node.js with Express.js server framework\n- **Development**: tsx for TypeScript execution in development\n- **API Design**: RESTful endpoints with JSON responses\n- **Data Storage**: In-memory storage with Map-based implementation (IStorage interface)\n- **Error Handling**: Centralized error middleware with structured responses\n- **Request Logging**: Custom middleware for API request/response logging\n\n## Data Layer\n- **ORM**: Drizzle ORM configured for PostgreSQL\n- **Schema**: Centralized schema definitions in shared directory\n- **Validation**: Zod schemas for runtime type validation\n- **Tables**: repair_guides, troubleshooting_flows, device_components\n- **Types**: Strongly typed with TypeScript interfaces\n\n## Key Features\n- **Device Support**: Laptop and desktop computer repair guides with brand/model specificity\n- **Category Organization**: Hardware, software, cleaning, and upgrade guides\n- **Difficulty Levels**: Easy, medium, hard with color-coded indicators\n- **Interactive Elements**: Clickable parts identification, step-by-step workflows\n- **Search Functionality**: Full-text search across repair guides and device models\n- **Bookmarking**: User can bookmark favorite guides\n- **View and Download Tracking**: Real-time counting of guide views and downloads with display in UI\n\n- **Troubleshooting Flows**: Interactive decision trees for problem diagnosis\n- **Device Finder**: Brand and model-specific guide compatibility system\n- **User Guide Creation**: Community-driven guide creation with preview and validation\n- **Offline Mode**: Local storage for guides, offline access, and sync capabilities\n- **Dark Mode**: Complete dark/light/AMOLED theme support with system preference detection\n- **Theme Toggle**: Dropdown menu in header for light/dark/AMOLED/system theme selection\n- **Software Links**: Direct links to essential diagnostic and repair software tools\n- **Quick Actions**: Floating action button with emergency help and quick access to tools\n- **Download Center**: Comprehensive download functionality for guides and videos with multiple format options\n\n- **Enhanced Offline Storage**: 50MB capacity with detailed breakdown by content type, video/image caching, and automated cleanup\n- **Dropdown Navigation**: Organized navigation menus with categorized sections for repair tools and content management\n- **Enhanced Theme Display**: Improved theme toggle with current theme display and detailed descriptions for each theme option\n- **Mobile Navigation**: Comprehensive mobile menu with categorized sections and user authentication integration\n- **Unified User Menu**: Combined theme toggle and user authentication into single dropdown menu for cleaner header design\n- **Streamlined Navigation**: Merged repair tools and content sections into unified \"Tools & Content\" dropdown for better organization\n- **Progressive Web App (PWA)**: Full PWA support with standalone APK capability, service worker for offline functionality, install prompts, and app manifest for mobile installation\n\n## Component Structure\n- **Layout Components**: Header with search, navigation, and branding\n- **Repair Components**: Guide cards, detail modals, category grids\n- **Interactive Elements**: Parts identification with hotspots, troubleshooting flowcharts\n- **UI Components**: Comprehensive set of accessible components from shadcn/ui\n\n# External Dependencies\n\n## Core Framework Dependencies\n- **@vitejs/plugin-react**: React support for Vite\n- **@tanstack/react-query**: Server state management\n- **wouter**: Lightweight client-side routing\n- **express**: Node.js web framework\n\n## Database and ORM\n- **drizzle-orm**: TypeScript ORM for SQL databases\n- **drizzle-kit**: Schema management and migrations\n- **@neondatabase/serverless**: Neon PostgreSQL serverless driver\n- **connect-pg-simple**: PostgreSQL session store\n\n## UI and Styling\n- **@radix-ui/***: Accessible UI primitives (dialog, dropdown, etc.)\n- **tailwindcss**: Utility-first CSS framework\n- **class-variance-authority**: Utility for creating variant-based components\n- **clsx**: Conditional className utility\n- **lucide-react**: Icon library\n\n## Form and Validation\n- **react-hook-form**: Performant form library\n- **@hookform/resolvers**: Resolvers for validation libraries\n- **zod**: TypeScript-first schema validation\n- **drizzle-zod**: Zod integration for Drizzle schemas\n\n## Development Tools\n- **tsx**: TypeScript execution for Node.js\n- **esbuild**: Fast JavaScript bundler for production\n- **@replit/vite-plugin-runtime-error-modal**: Development error overlay\n- **@replit/vite-plugin-cartographer**: Replit integration for development\n\n## Utility Libraries\n- **date-fns**: Date manipulation library\n- **nanoid**: URL-safe unique string ID generator\n- **cmdk**: Command palette component\n- **embla-carousel-react**: Carousel component library","size_bytes":6371},"tailwind.config.ts":{"content":"import type { Config } from \"tailwindcss\";\n\nexport default {\n darkMode: [\"class\"],\n content: [\"./client/index.html\", \"./client/src/**/*.{js,jsx,ts,tsx}\"],\n theme: {\n extend: {\n borderRadius: {\n lg: \"var(--radius)\",\n md: \"calc(var(--radius) - 2px)\",\n sm: \"calc(var(--radius) - 4px)\",\n },\n colors: {\n background: \"var(--background)\",\n foreground: \"var(--foreground)\",\n card: {\n DEFAULT: \"var(--card)\",\n foreground: \"var(--card-foreground)\",\n },\n popover: {\n DEFAULT: \"var(--popover)\",\n foreground: \"var(--popover-foreground)\",\n },\n primary: {\n DEFAULT: \"var(--primary)\",\n foreground: \"var(--primary-foreground)\",\n 700: \"var(--primary-700)\",\n 900: \"var(--primary-900)\",\n },\n secondary: {\n DEFAULT: \"var(--secondary)\",\n foreground: \"var(--secondary-foreground)\",\n },\n muted: {\n DEFAULT: \"var(--muted)\",\n foreground: \"var(--muted-foreground)\",\n },\n accent: {\n DEFAULT: \"var(--accent)\",\n foreground: \"var(--accent-foreground)\",\n },\n destructive: {\n DEFAULT: \"var(--destructive)\",\n foreground: \"var(--destructive-foreground)\",\n },\n border: \"var(--border)\",\n input: \"var(--input)\",\n ring: \"var(--ring)\",\n success: {\n DEFAULT: \"var(--success)\",\n 600: \"var(--success-600)\",\n },\n warning: {\n DEFAULT: \"var(--warning)\",\n 700: \"var(--warning-700)\",\n },\n danger: {\n DEFAULT: \"var(--danger)\",\n 700: \"var(--danger-700)\",\n },\n purple: {\n 600: \"var(--purple-600)\",\n },\n cyan: {\n 600: \"var(--cyan-600)\",\n },\n },\n fontFamily: {\n roboto: [\"var(--font-roboto)\"],\n \"roboto-mono\": [\"var(--font-roboto-mono)\"],\n sans: [\"var(--font-roboto)\"],\n serif: [\"Georgia\", \"serif\"],\n mono: [\"var(--font-roboto-mono)\"],\n },\n boxShadow: {\n material: \"var(--shadow-material)\",\n \"material-lg\": \"var(--shadow-material-lg)\",\n },\n keyframes: {\n \"accordion-down\": {\n from: {\n height: \"0\",\n },\n to: {\n height: \"var(--radix-accordion-content-height)\",\n },\n },\n \"accordion-up\": {\n from: {\n height: \"var(--radix-accordion-content-height)\",\n },\n to: {\n height: \"0\",\n },\n },\n },\n animation: {\n \"accordion-down\": \"accordion-down 0.2s ease-out\",\n \"accordion-up\": \"accordion-up 0.2s ease-out\",\n },\n },\n },\n plugins: [require(\"tailwindcss-animate\"), require(\"@tailwindcss/typography\")],\n} satisfies Config;\n","size_bytes":2871},"vite.config.ts":{"content":"import { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\nimport path from \"path\";\nimport runtimeErrorOverlay from \"@replit/vite-plugin-runtime-error-modal\";\n\nexport default defineConfig({\n plugins: [\n react(),\n runtimeErrorOverlay(),\n ...(process.env.NODE_ENV !== \"production\" &&\n process.env.REPL_ID !== undefined\n ? [\n await import(\"@replit/vite-plugin-cartographer\").then((m) =>\n m.cartographer(),\n ),\n ]\n : []),\n ],\n resolve: {\n alias: {\n \"@\": path.resolve(import.meta.dirname, \"client\", \"src\"),\n \"@shared\": path.resolve(import.meta.dirname, \"shared\"),\n \"@assets\": path.resolve(import.meta.dirname, \"attached_assets\"),\n },\n },\n root: path.resolve(import.meta.dirname, \"client\"),\n build: {\n outDir: path.resolve(import.meta.dirname, \"dist/public\"),\n emptyOutDir: true,\n },\n server: {\n fs: {\n strict: true,\n deny: [\"**/.*\"],\n },\n },\n});\n","size_bytes":971},"server/db.ts":{"content":"import { Pool, neonConfig } from '@neondatabase/serverless';\nimport { drizzle } from 'drizzle-orm/neon-serverless';\nimport ws from \"ws\";\nimport * as schema from \"@shared/schema\";\n\nneonConfig.webSocketConstructor = ws;\n\nif (!process.env.DATABASE_URL) {\n throw new Error(\n \"DATABASE_URL must be set. Did you forget to provision a database?\",\n );\n}\n\nexport const pool = new Pool({ connectionString: process.env.DATABASE_URL });\nexport const db = drizzle({ client: pool, schema });","size_bytes":482},"server/index.ts":{"content":"import express, { type Request, Response, NextFunction } from \"express\";\nimport { registerRoutes } from \"./routes\";\nimport { setupVite, serveStatic, log } from \"./vite\";\nimport { seedDatabase } from \"./seed-data\";\n\nconst app = express();\napp.use(express.json());\napp.use(express.urlencoded({ extended: false }));\n\napp.use((req, res, next) => {\n const start = Date.now();\n const path = req.path;\n let capturedJsonResponse: Record<string, any> | undefined = undefined;\n\n const originalResJson = res.json;\n res.json = function (bodyJson, ...args) {\n capturedJsonResponse = bodyJson;\n return originalResJson.apply(res, [bodyJson, ...args]);\n };\n\n res.on(\"finish\", () => {\n const duration = Date.now() - start;\n if (path.startsWith(\"/api\")) {\n let logLine = `${req.method} ${path} ${res.statusCode} in ${duration}ms`;\n if (capturedJsonResponse) {\n logLine += ` :: ${JSON.stringify(capturedJsonResponse)}`;\n }\n\n if (logLine.length > 80) {\n logLine = logLine.slice(0, 79) + \"…\";\n }\n\n log(logLine);\n }\n });\n\n next();\n});\n\n(async () => {\n const server = await registerRoutes(app);\n \n // Seed database in background after server starts (non-blocking)\n setImmediate(async () => {\n try {\n await seedDatabase();\n } catch (error) {\n console.error(\"Error seeding database:\", error);\n }\n });\n\n app.use((err: any, _req: Request, res: Response, _next: NextFunction) => {\n const status = err.status || err.statusCode || 500;\n const message = err.message || \"Internal Server Error\";\n\n res.status(status).json({ message });\n throw err;\n });\n\n // importantly only setup vite in development and after\n // setting up all the other routes so the catch-all route\n // doesn't interfere with the other routes\n if (app.get(\"env\") === \"development\") {\n await setupVite(app, server);\n } else {\n serveStatic(app);\n }\n\n // ALWAYS serve the app on the port specified in the environment variable PORT\n // Other ports are firewalled. Default to 5000 if not specified.\n // this serves both the API and the client.\n // It is the only port that is not firewalled.\n const port = parseInt(process.env.PORT || '5000', 10);\n server.listen({\n port,\n host: '0.0.0.0', // Important for deployment - allows external connections\n reusePort: true,\n }, () => {\n log(`serving on port ${port}`);\n });\n})();\n","size_bytes":2395},"server/replitAuth.ts":{"content":"import * as client from \"openid-client\";\nimport { Strategy, type VerifyFunction } from \"openid-client/passport\";\n\nimport passport from \"passport\";\nimport session from \"express-session\";\nimport type { Express, RequestHandler } from \"express\";\nimport memoize from \"memoizee\";\nimport connectPg from \"connect-pg-simple\";\nimport { storage } from \"./storage\";\n\nif (!process.env.REPLIT_DOMAINS) {\n throw new Error(\"Environment variable REPLIT_DOMAINS not provided\");\n}\n\nconst getOidcConfig = memoize(\n async () => {\n return await client.discovery(\n new URL(process.env.ISSUER_URL ?? \"https://replit.com/oidc\"),\n process.env.REPL_ID!\n );\n },\n { maxAge: 3600 * 1000 }\n);\n\nexport function getSession() {\n const sessionTtl = 7 * 24 * 60 * 60 * 1000; // 1 week\n const pgStore = connectPg(session);\n const sessionStore = new pgStore({\n conString: process.env.DATABASE_URL,\n createTableIfMissing: false,\n ttl: sessionTtl,\n tableName: \"sessions\",\n });\n return session({\n secret: process.env.SESSION_SECRET!,\n store: sessionStore,\n resave: false,\n saveUninitialized: false,\n cookie: {\n httpOnly: true,\n secure: true,\n maxAge: sessionTtl,\n },\n });\n}\n\nfunction updateUserSession(\n user: any,\n tokens: client.TokenEndpointResponse & client.TokenEndpointResponseHelpers\n) {\n user.claims = tokens.claims();\n user.access_token = tokens.access_token;\n user.refresh_token = tokens.refresh_token;\n user.expires_at = user.claims?.exp;\n}\n\nasync function upsertUser(\n claims: any,\n) {\n // Set role based on email - jhustle44@gmail.com gets admin privileges\n const role = claims[\"email\"] === \"jhustle44@gmail.com\" ? \"admin\" : \"user\";\n \n await storage.upsertUser({\n id: claims[\"sub\"],\n email: claims[\"email\"],\n firstName: claims[\"first_name\"],\n lastName: claims[\"last_name\"],\n profileImageUrl: claims[\"profile_image_url\"],\n role: role,\n });\n}\n\nexport async function setupAuth(app: Express) {\n app.set(\"trust proxy\", 1);\n app.use(getSession());\n app.use(passport.initialize());\n app.use(passport.session());\n\n const config = await getOidcConfig();\n\n const verify: VerifyFunction = async (\n tokens: client.TokenEndpointResponse & client.TokenEndpointResponseHelpers,\n verified: passport.AuthenticateCallback\n ) => {\n const user = {};\n updateUserSession(user, tokens);\n await upsertUser(tokens.claims());\n verified(null, user);\n };\n\n for (const domain of process.env\n .REPLIT_DOMAINS!.split(\",\")) {\n const strategy = new Strategy(\n {\n name: `replitauth:${domain}`,\n config,\n scope: \"openid email profile offline_access\",\n callbackURL: `https://${domain}/api/callback`,\n },\n verify,\n );\n passport.use(strategy);\n }\n\n passport.serializeUser((user: Express.User, cb) => cb(null, user));\n passport.deserializeUser((user: Express.User, cb) => cb(null, user));\n\n app.get(\"/api/login\", (req, res, next) => {\n passport.authenticate(`replitauth:${req.hostname}`, {\n prompt: \"login consent\",\n scope: [\"openid\", \"email\", \"profile\", \"offline_access\"],\n })(req, res, next);\n });\n\n app.get(\"/api/callback\", (req, res, next) => {\n passport.authenticate(`replitauth:${req.hostname}`, {\n successReturnToOrRedirect: \"/\",\n failureRedirect: \"/api/login\",\n })(req, res, next);\n });\n\n app.get(\"/api/logout\", (req, res) => {\n req.logout(() => {\n res.redirect(\n client.buildEndSessionUrl(config, {\n client_id: process.env.REPL_ID!,\n post_logout_redirect_uri: `${req.protocol}://${req.hostname}`,\n }).href\n );\n });\n });\n}\n\nexport const isAuthenticated: RequestHandler = async (req, res, next) => {\n const user = req.user as any;\n\n if (!req.isAuthenticated() || !user.expires_at) {\n return res.status(401).json({ message: \"Unauthorized\" });\n }\n\n const now = Math.floor(Date.now() / 1000);\n if (now <= user.expires_at) {\n return next();\n }\n\n const refreshToken = user.refresh_token;\n if (!refreshToken) {\n res.status(401).json({ message: \"Unauthorized\" });\n return;\n }\n\n try {\n const config = await getOidcConfig();\n const tokenResponse = await client.refreshTokenGrant(config, refreshToken);\n updateUserSession(user, tokenResponse);\n return next();\n } catch (error) {\n res.status(401).json({ message: \"Unauthorized\" });\n return;\n }\n};","size_bytes":4389},"server/routes.ts":{"content":"import type { Express } from \"express\";\nimport { createServer, type Server } from \"http\";\nimport { storage } from \"./storage\";\nimport { insertRepairGuideSchema, insertTroubleshootingFlowSchema, insertDeviceComponentSchema, insertFavoriteSchema } from \"@shared/schema\";\nimport { setupAuth, isAuthenticated } from \"./replitAuth\";\nimport { requireAdmin } from \"./middleware/adminAuth\";\n\nexport async function registerRoutes(app: Express): Promise<Server> {\n // Auth middleware\n await setupAuth(app);\n\n // Auth routes\n app.get('/api/auth/user', isAuthenticated, async (req: any, res) => {\n try {\n const userId = req.user.claims.sub;\n const user = await storage.getUser(userId);\n res.json(user);\n } catch (error) {\n console.error(\"Error fetching user:\", error);\n res.status(500).json({ message: \"Failed to fetch user\" });\n }\n });\n\n // Favorites routes\n app.get('/api/favorites', isAuthenticated, async (req: any, res) => {\n try {\n const userId = req.user.claims.sub;\n const favorites = await storage.getUserFavorites(userId);\n res.json(favorites);\n } catch (error) {\n console.error(\"Error fetching favorites:\", error);\n res.status(500).json({ message: \"Failed to fetch favorites\" });\n }\n });\n\n app.post('/api/favorites', isAuthenticated, async (req: any, res) => {\n try {\n const userId = req.user.claims.sub;\n const favoriteData = insertFavoriteSchema.parse(req.body);\n const favorite = await storage.addToFavorites(userId, favoriteData);\n res.json(favorite);\n } catch (error) {\n console.error(\"Error adding favorite:\", error);\n res.status(500).json({ message: \"Failed to add favorite\" });\n }\n });\n\n app.delete('/api/favorites/:itemId/:itemType', isAuthenticated, async (req: any, res) => {\n try {\n const userId = req.user.claims.sub;\n const { itemId, itemType } = req.params;\n await storage.removeFromFavorites(userId, itemId, itemType);\n res.json({ success: true });\n } catch (error) {\n console.error(\"Error removing favorite:\", error);\n res.status(500).json({ message: \"Failed to remove favorite\" });\n }\n });\n\n app.get('/api/favorites/check/:itemId/:itemType', isAuthenticated, async (req: any, res) => {\n try {\n const userId = req.user.claims.sub;\n const { itemId, itemType } = req.params;\n const isFavorited = await storage.isFavorited(userId, itemId, itemType);\n res.json({ isFavorited });\n } catch (error) {\n console.error(\"Error checking favorite:\", error);\n res.status(500).json({ message: \"Failed to check favorite status\" });\n }\n });\n // Repair Guides\n app.get(\"/api/repair-guides\", async (req, res) => {\n try {\n const { deviceType, category, difficulty } = req.query;\n const guides = await storage.getRepairGuides({\n deviceType: deviceType as string,\n category: category as string,\n difficulty: difficulty as string,\n });\n res.json(guides);\n } catch (error) {\n res.status(500).json({ message: \"Failed to fetch repair guides\" });\n }\n });\n\n app.get(\"/api/repair-guides/search\", async (req, res) => {\n try {\n const { q } = req.query;\n if (!q || typeof q !== \"string\") {\n return res.status(400).json({ message: \"Search query is required\" });\n }\n const guides = await storage.searchRepairGuides(q);\n res.json(guides);\n } catch (error) {\n res.status(500).json({ message: \"Failed to search repair guides\" });\n }\n });\n\n app.get(\"/api/repair-guides/:id\", async (req, res) => {\n try {\n const guide = await storage.getRepairGuide(req.params.id);\n if (!guide) {\n return res.status(404).json({ message: \"Repair guide not found\" });\n }\n // Increment view count when guide is viewed\n await storage.updateViewCount(req.params.id);\n res.json(guide);\n } catch (error) {\n res.status(500).json({ message: \"Failed to fetch repair guide\" });\n }\n });\n\n app.post(\"/api/repair-guides\", async (req, res) => {\n try {\n const validatedData = insertRepairGuideSchema.parse(req.body);\n const guide = await storage.createRepairGuide(validatedData);\n res.status(201).json(guide);\n } catch (error) {\n res.status(400).json({ message: \"Invalid repair guide data\" });\n }\n });\n\n // User-created guides endpoint\n app.post(\"/api/user-guides\", async (req, res) => {\n try {\n const validatedData = insertRepairGuideSchema.parse(req.body);\n const guide = await storage.createUserGuide(validatedData);\n res.status(201).json(guide);\n } catch (error) {\n console.error(\"Error creating user guide:\", error);\n res.status(400).json({ message: \"Invalid user guide data\" });\n }\n });\n\n // Track download for a specific guide\n app.post(\"/api/repair-guides/:id/download\", async (req, res) => {\n try {\n const guide = await storage.getRepairGuide(req.params.id);\n if (!guide) {\n return res.status(404).json({ message: \"Repair guide not found\" });\n }\n // Increment download count when guide is downloaded\n await storage.updateDownloadCount(req.params.id);\n res.json({ success: true, message: \"Download tracked\" });\n } catch (error) {\n res.status(500).json({ message: \"Failed to track download\" });\n }\n });\n\n // Version endpoint\n app.get(\"/api/version\", async (req, res) => {\n try {\n const versionInfo = {\n version: \"1.0.0\",\n name: \"JCR Guide Pro\",\n build: \"2025.08.18\",\n release: \"Major Release\"\n };\n res.json(versionInfo);\n } catch (error) {\n res.status(500).json({ message: \"Failed to fetch version info\" });\n }\n });\n\n // Admin routes\n app.get(\"/api/admin/users\", requireAdmin, async (req, res) => {\n try {\n // This would need a getAllUsers method in storage - for now return basic info\n res.json({ message: \"Admin access confirmed\", adminEmail: req.adminUser?.email });\n } catch (error) {\n res.status(500).json({ message: \"Failed to fetch admin data\" });\n }\n });\n\n app.get(\"/api/admin/stats\", requireAdmin, async (req, res) => {\n try {\n const guides = await storage.getRepairGuides();\n const totalViews = guides.reduce((sum, guide) => sum + (guide.viewCount || 0), 0);\n const totalDownloads = guides.reduce((sum, guide) => sum + (guide.downloadCount || 0), 0);\n \n res.json({\n totalGuides: guides.length,\n totalViews,\n totalDownloads,\n adminUser: req.adminUser?.email\n });\n } catch (error) {\n res.status(500).json({ message: \"Failed to fetch admin stats\" });\n }\n });\n\n // Bulk download endpoint\n app.post(\"/api/download/bulk\", async (req, res) => {\n try {\n const { type, filters } = req.body;\n let data: any[] = [];\n\n switch (type) {\n case 'repair-guides':\n data = await storage.getRepairGuides(filters || {});\n break;\n case 'troubleshooting-flows':\n data = await storage.getTroubleshootingFlows();\n break;\n case 'device-components':\n data = await storage.getDeviceComponents();\n break;\n case 'complete':\n const [guides, flows, components] = await Promise.all([\n storage.getRepairGuides({}),\n storage.getTroubleshootingFlows(),\n storage.getDeviceComponents()\n ]);\n const exportData = {\n exportInfo: {\n title: 'JCR Guide Pro Complete Export',\n version: '1.0',\n exportedAt: new Date().toISOString(),\n totalItems: guides.length + flows.length + components.length\n },\n repairGuides: guides,\n troubleshootingFlows: flows,\n deviceComponents: components\n };\n data = exportData as any;\n break;\n default:\n return res.status(400).json({ message: \"Invalid download type\" });\n }\n\n res.json({\n success: true,\n data,\n count: Array.isArray(data) ? data.length : Object.keys(data).length - 1,\n timestamp: new Date().toISOString()\n });\n } catch (error) {\n console.error(\"Bulk download error:\", error);\n res.status(500).json({ message: \"Failed to prepare bulk download\" });\n }\n });\n\n app.patch(\"/api/repair-guides/:id/bookmark\", async (req, res) => {\n try {\n const guide = await storage.toggleBookmark(req.params.id);\n if (!guide) {\n return res.status(404).json({ message: \"Repair guide not found\" });\n }\n res.json(guide);\n } catch (error) {\n res.status(500).json({ message: \"Failed to toggle bookmark\" });\n }\n });\n\n // Troubleshooting Flows\n app.get(\"/api/troubleshooting-flows\", async (req, res) => {\n try {\n const flows = await storage.getTroubleshootingFlows();\n res.json(flows);\n } catch (error) {\n res.status(500).json({ message: \"Failed to fetch troubleshooting flows\" });\n }\n });\n\n app.get(\"/api/troubleshooting-flows/:id\", async (req, res) => {\n try {\n const flow = await storage.getTroubleshootingFlow(req.params.id);\n if (!flow) {\n return res.status(404).json({ message: \"Troubleshooting flow not found\" });\n }\n res.json(flow);\n } catch (error) {\n res.status(500).json({ message: \"Failed to fetch troubleshooting flow\" });\n }\n });\n\n app.post(\"/api/troubleshooting-flows\", async (req, res) => {\n try {\n const validatedData = insertTroubleshootingFlowSchema.parse(req.body);\n const flow = await storage.createTroubleshootingFlow(validatedData);\n res.status(201).json(flow);\n } catch (error) {\n res.status(400).json({ message: \"Invalid troubleshooting flow data\" });\n }\n });\n\n // Device Components\n app.get(\"/api/device-components\", async (req, res) => {\n try {\n const { deviceType } = req.query;\n const components = await storage.getDeviceComponents(deviceType as string);\n res.json(components);\n } catch (error) {\n res.status(500).json({ message: \"Failed to fetch device components\" });\n }\n });\n\n app.get(\"/api/device-components/:id\", async (req, res) => {\n try {\n const component = await storage.getDeviceComponent(req.params.id);\n if (!component) {\n return res.status(404).json({ message: \"Device component not found\" });\n }\n res.json(component);\n } catch (error) {\n res.status(500).json({ message: \"Failed to fetch device component\" });\n }\n });\n\n app.post(\"/api/device-components\", async (req, res) => {\n try {\n const validatedData = insertDeviceComponentSchema.parse(req.body);\n const component = await storage.createDeviceComponent(validatedData);\n res.status(201).json(component);\n } catch (error) {\n res.status(400).json({ message: \"Invalid device component data\" });\n }\n });\n\n // Device Brands routes\n app.get(\"/api/device-brands\", async (req, res) => {\n try {\n const { deviceType } = req.query;\n const brands = await storage.getDeviceBrands(deviceType as string);\n res.json(brands);\n } catch (error) {\n console.error(\"Error fetching device brands:\", error);\n res.status(500).json({ message: \"Failed to fetch device brands\" });\n }\n });\n\n // Device Models routes\n app.get(\"/api/device-models\", async (req, res) => {\n try {\n const { brandId, deviceType, search } = req.query;\n \n if (search) {\n const models = await storage.searchDeviceModels(search as string, deviceType as string);\n res.json(models);\n } else {\n const models = await storage.getDeviceModels(brandId as string, deviceType as string);\n res.json(models);\n }\n } catch (error) {\n console.error(\"Error fetching device models:\", error);\n res.status(500).json({ message: \"Failed to fetch device models\" });\n }\n });\n\n app.get(\"/api/device-models/:id\", async (req, res) => {\n try {\n const { id } = req.params;\n const model = await storage.getDeviceModel(id);\n if (!model) {\n return res.status(404).json({ message: \"Device model not found\" });\n }\n res.json(model);\n } catch (error) {\n console.error(\"Error fetching device model:\", error);\n res.status(500).json({ message: \"Failed to fetch device model\" });\n }\n });\n\n app.get(\"/api/brands/:brandId/models\", async (req, res) => {\n try {\n const { brandId } = req.params;\n const models = await storage.getModelsByBrand(brandId);\n res.json(models);\n } catch (error) {\n console.error(\"Error fetching models by brand:\", error);\n res.status(500).json({ message: \"Failed to fetch models by brand\" });\n }\n });\n\n // Guide Compatibility routes\n app.get(\"/api/models/:modelId/guides\", async (req, res) => {\n try {\n const { modelId } = req.params;\n const guides = await storage.getCompatibleGuides(modelId);\n res.json(guides);\n } catch (error) {\n console.error(\"Error fetching compatible guides:\", error);\n res.status(500).json({ message: \"Failed to fetch compatible guides\" });\n }\n });\n\n app.get(\"/api/guides/:guideId/models\", async (req, res) => {\n try {\n const { guideId } = req.params;\n const models = await storage.getModelCompatibility(guideId);\n res.json(models);\n } catch (error) {\n console.error(\"Error fetching model compatibility:\", error);\n res.status(500).json({ message: \"Failed to fetch model compatibility\" });\n }\n });\n\n const httpServer = createServer(app);\n return httpServer;\n}\n","size_bytes":13576},"server/seed-data.ts":{"content":"import { db } from \"./db\";\nimport { repairGuides, troubleshootingFlows, deviceComponents } from \"@shared/schema\";\n\nconst comprehensiveRepairGuides = [\n // LAPTOP HARDWARE GUIDES\n {\n title: \"Replace Laptop RAM Memory\",\n description: \"Complete guide to upgrading or replacing laptop RAM memory modules for improved performance.\",\n deviceType: \"laptop\",\n category: \"hardware\",\n difficulty: \"easy\",\n estimatedTime: \"15-20 min\",\n toolsRequired: [\"Phillips head screwdriver\", \"Anti-static wrist strap\", \"Clean cloth\"],\n safetyWarnings: [\n \"Power off laptop and remove battery before starting\",\n \"Use anti-static protection to prevent component damage\",\n \"Handle RAM modules by edges only\"\n ],\n steps: [\n {\n stepNumber: 1,\n title: \"Prepare the laptop\",\n description: \"Shut down completely, unplug power, and remove battery if removable\",\n imageUrl: \"/images/steps/laptop-preparation.svg\",\n tips: [\"Wait 5 minutes after shutdown to ensure complete power discharge\"]\n },\n {\n stepNumber: 2,\n title: \"Access RAM compartment\",\n description: \"Remove the bottom panel screws and locate RAM slots\",\n imageUrl: \"/images/steps/ram-access.svg\",\n tips: [\"Keep screws organized to avoid losing them\"]\n },\n {\n stepNumber: 3,\n title: \"Remove old RAM\",\n description: \"Gently push side clips and pull module at 45-degree angle\",\n imageUrl: \"/images/steps/ram-removal.svg\",\n tips: [\"If resistance is felt, double-check clip positions\"]\n },\n {\n stepNumber: 4,\n title: \"Install new RAM\",\n description: \"Insert new module at 45-degree angle and press down until clips engage\",\n imageUrl: \"/images/steps/ram-installation.svg\",\n tips: [\"Ensure module is fully seated and both clips are engaged\"]\n }\n ],\n alternativeSolutions: \"Consider upgrading to higher capacity modules if motherboard supports them\",\n imageUrl: \"/images/guides/laptop-ram-upgrade.svg\"\n },\n {\n title: \"Replace Laptop Hard Drive with SSD\",\n description: \"Step-by-step guide to replace traditional hard drive with faster SSD storage.\",\n deviceType: \"laptop\",\n category: \"hardware\",\n difficulty: \"medium\",\n estimatedTime: \"45-60 min\",\n toolsRequired: [\"Phillips head screwdriver\", \"SATA to USB adapter\", \"Cloning software\", \"Anti-static wrist strap\"],\n safetyWarnings: [\n \"Create data backup before starting procedure\",\n \"Ensure laptop is completely powered off\",\n \"Handle storage devices with care to prevent damage\"\n ],\n steps: [\n {\n stepNumber: 1,\n title: \"Backup existing data\",\n description: \"Create complete backup of current hard drive using cloning software\",\n imageUrl: \"/images/steps/data-backup.svg\",\n tips: [\"Use reliable cloning software like Clonezilla or manufacturer tools\"]\n },\n {\n stepNumber: 2,\n title: \"Access hard drive bay\",\n description: \"Remove laptop bottom panel and locate hard drive compartment\",\n imageUrl: \"/images/steps/hdd-access.svg\",\n tips: [\"Some laptops have dedicated HDD access panels\"]\n },\n {\n stepNumber: 3,\n title: \"Remove old hard drive\",\n description: \"Disconnect SATA connector and remove mounting screws\",\n imageUrl: \"/images/steps/hdd-removal.svg\",\n tips: [\"Note orientation of drive for proper SSD installation\"]\n },\n {\n stepNumber: 4,\n title: \"Install SSD\",\n description: \"Mount SSD in same position and reconnect SATA connector\",\n imageUrl: \"/images/steps/ssd-installation.svg\",\n tips: [\"Ensure secure connection and proper mounting\"]\n },\n {\n stepNumber: 5,\n title: \"Restore data\",\n description: \"Boot from cloned drive or restore backup to new SSD\",\n imageUrl: \"/images/steps/data-restore.svg\",\n tips: [\"Verify all data transferred correctly before disposing old drive\"]\n }\n ],\n alternativeSolutions: \"Consider using external drive enclosure for old HDD as additional storage\",\n imageUrl: \"/images/guides/laptop-ssd-upgrade.svg\"\n },\n {\n title: \"Fix Laptop Screen Flickering\",\n description: \"Diagnose and repair common laptop screen flickering issues including cable connections.\",\n deviceType: \"laptop\",\n category: \"hardware\",\n difficulty: \"hard\",\n estimatedTime: \"60-90 min\",\n toolsRequired: [\"Phillips head screwdriver\", \"Plastic prying tools\", \"Multimeter\", \"Replacement LVDS cable\"],\n safetyWarnings: [\n \"LCD panels are fragile - handle with extreme care\",\n \"Ensure laptop is powered off and battery removed\",\n \"Be gentle with ribbon cables to prevent tearing\"\n ],\n steps: [\n {\n stepNumber: 1,\n title: \"Initial diagnosis\",\n description: \"Test with external monitor to isolate display vs graphics issue\",\n imageUrl: \"/images/steps/external-monitor-test.svg\",\n tips: [\"If external display works fine, problem is likely in LCD assembly\"]\n },\n {\n stepNumber: 2,\n title: \"Access LCD assembly\",\n description: \"Remove screen bezel and access LCD panel connections\",\n imageUrl: \"/images/steps/lcd-bezel-removal.svg\",\n tips: [\"Use plastic tools to avoid scratching bezel\"]\n },\n {\n stepNumber: 3,\n title: \"Check LVDS cable\",\n description: \"Inspect and reseat the LVDS cable connection\",\n imageUrl: \"/images/steps/lvds-cable-check.svg\",\n tips: [\"Look for pinched, bent, or damaged cable sections\"]\n },\n {\n stepNumber: 4,\n title: \"Test connections\",\n description: \"Use multimeter to verify cable continuity if problem persists\",\n imageUrl: \"/images/steps/cable-continuity-test.svg\",\n tips: [\"Compare readings with known good cable if available\"]\n },\n {\n stepNumber: 5,\n title: \"Replace if necessary\",\n description: \"Install new LVDS cable if old one is damaged\",\n imageUrl: \"/images/steps/lvds-cable-replacement.svg\",\n tips: [\"Route new cable exactly as original to prevent future issues\"]\n }\n ],\n alternativeSolutions: \"If flickering persists, issue may be with LCD panel itself requiring replacement\",\n imageUrl: \"/images/guides/laptop-screen-repair.svg\"\n },\n {\n title: \"Replace Laptop Keyboard\",\n description: \"Complete guide to replacing damaged or non-functioning laptop keyboard.\",\n deviceType: \"laptop\",\n category: \"hardware\",\n difficulty: \"medium\",\n estimatedTime: \"30-45 min\",\n toolsRequired: [\"Phillips head screwdriver\", \"Plastic prying tools\", \"Replacement keyboard\"],\n safetyWarnings: [\n \"Power off laptop completely before starting\",\n \"Be gentle with ribbon cable connectors\",\n \"Keep track of small screws and clips\"\n ],\n steps: [\n {\n stepNumber: 1,\n title: \"Remove keyboard screws\",\n description: \"Locate and remove screws securing keyboard (usually on bottom panel)\",\n imageUrl: \"/images/steps/keyboard-screws.svg\",\n tips: [\"Some keyboards have screws under battery compartment\"]\n },\n {\n stepNumber: 2,\n title: \"Lift keyboard\",\n description: \"Carefully lift keyboard from top edge and tilt forward\",\n imageUrl: \"/images/steps/keyboard-lift.svg\",\n tips: [\"Don't force - some models have additional clips\"]\n },\n {\n stepNumber: 3,\n title: \"Disconnect ribbon cable\",\n description: \"Carefully disconnect keyboard ribbon cable connector\",\n imageUrl: \"/images/steps/keyboard-disconnect.svg\",\n tips: [\"Note cable orientation for proper reconnection\"]\n },\n {\n stepNumber: 4,\n title: \"Install new keyboard\",\n description: \"Connect new keyboard cable and secure in proper position\",\n imageUrl: \"/images/steps/keyboard-install.svg\",\n tips: [\"Ensure cable is fully seated before closing connector\"]\n }\n ],\n alternativeSolutions: \"If only specific keys are problematic, consider individual key cap replacement\",\n imageUrl: \"/images/guides/laptop-keyboard-replacement.svg\"\n },\n\n // LAPTOP SOFTWARE GUIDES\n {\n title: \"Remove Malware and Viruses\",\n description: \"Comprehensive guide to detecting, removing, and preventing malware infections.\",\n deviceType: \"laptop\",\n category: \"software\",\n difficulty: \"medium\",\n estimatedTime: \"60-120 min\",\n toolsRequired: [\"Antivirus software\", \"Anti-malware scanner\", \"Recovery media\", \"Internet connection\"],\n safetyWarnings: [\n \"Disconnect from internet if actively infected\",\n \"Backup important data before cleaning\",\n \"Use reputable security software only\"\n ],\n steps: [\n {\n stepNumber: 1,\n title: \"Boot into Safe Mode\",\n description: \"Restart computer and boot into Safe Mode to prevent malware from running\",\n imageUrl: \"/images/steps/safe-mode-boot.svg\",\n tips: [\"Press F8 during startup or use advanced startup options\"]\n },\n {\n stepNumber: 2,\n title: \"Run full system scan\",\n description: \"Use updated antivirus software to perform complete system scan\",\n imageUrl: \"/images/steps/antivirus-scan.svg\",\n tips: [\"Update virus definitions before scanning\"]\n },\n {\n stepNumber: 3,\n title: \"Use specialized tools\",\n description: \"Run additional anti-malware tools like Malwarebytes for thorough cleaning\",\n imageUrl: \"/images/steps/malware-removal.svg\",\n tips: [\"Use multiple tools as different software detects different threats\"]\n },\n {\n stepNumber: 4,\n title: \"Clean browser settings\",\n description: \"Reset browser settings and remove malicious extensions\",\n imageUrl: \"/images/steps/browser-cleanup.svg\",\n tips: [\"Check all installed browsers, not just the primary one\"]\n },\n {\n stepNumber: 5,\n title: \"Update and secure\",\n description: \"Install all system updates and configure security settings\",\n imageUrl: \"/images/steps/system-security.svg\",\n tips: [\"Enable automatic updates and configure firewall\"]\n }\n ],\n alternativeSolutions: \"For severe infections, consider complete OS reinstallation\",\n imageUrl: \"/images/guides/malware-removal.svg\"\n },\n {\n title: \"Fix Blue Screen of Death (BSOD)\",\n description: \"Troubleshoot and resolve Windows Blue Screen errors with systematic approach.\",\n deviceType: \"laptop\",\n category: \"software\",\n difficulty: \"hard\",\n estimatedTime: \"45-90 min\",\n toolsRequired: [\"Windows installation media\", \"Driver software\", \"System information tools\"],\n safetyWarnings: [\n \"Note exact error code before restarting\",\n \"Backup important data when possible\",\n \"Avoid repeated forced shutdowns\"\n ],\n steps: [\n {\n stepNumber: 1,\n title: \"Record error information\",\n description: \"Note the stop code and any file names mentioned in BSOD\",\n imageUrl: \"/images/steps/bsod-error-code.svg\",\n tips: [\"Take photo of screen if error appears quickly\"]\n },\n {\n stepNumber: 2,\n title: \"Boot into Safe Mode\",\n description: \"Start computer in Safe Mode to access system for troubleshooting\",\n imageUrl: \"/images/steps/safe-mode-access.svg\",\n tips: [\"Use startup repair if Safe Mode won't load\"]\n },\n {\n stepNumber: 3,\n title: \"Check recent changes\",\n description: \"Review recently installed software, drivers, or hardware changes\",\n imageUrl: \"/images/steps/recent-changes-check.svg\",\n tips: [\"Use System Restore to undo recent changes\"]\n },\n {\n stepNumber: 4,\n title: \"Update or rollback drivers\",\n description: \"Update problematic drivers or rollback to previous versions\",\n imageUrl: \"/images/steps/driver-management.svg\",\n tips: [\"Focus on display, storage, and network drivers first\"]\n },\n {\n stepNumber: 5,\n title: \"Check hardware\",\n description: \"Run memory test and check hard drive for errors\",\n imageUrl: \"/images/steps/hardware-diagnostics.svg\",\n tips: [\"Use Windows Memory Diagnostic and chkdsk utility\"]\n }\n ],\n alternativeSolutions: \"Consider reinstalling Windows if BSOD persists after troubleshooting\",\n imageUrl: \"/images/guides/bsod-troubleshooting.svg\"\n },\n {\n title: \"Fix Laptop Won't Boot\",\n description: \"Systematic troubleshooting for laptops that fail to start or boot properly.\",\n deviceType: \"laptop\",\n category: \"software\",\n difficulty: \"medium\",\n estimatedTime: \"45-75 min\",\n toolsRequired: [\"Bootable USB drive\", \"Windows installation media\", \"External keyboard\"],\n safetyWarnings: [\n \"Try simple solutions first before advanced troubleshooting\",\n \"Have important data backed up before making changes\",\n \"Note any error messages or beep codes\"\n ],\n steps: [\n {\n stepNumber: 1,\n title: \"Check power and hardware\",\n description: \"Verify power adapter, battery, and basic hardware connections\",\n imageUrl: \"/images/steps/power-check.svg\",\n tips: [\"Try with just AC adapter, no battery\"]\n },\n {\n stepNumber: 2,\n title: \"Access BIOS/UEFI\",\n description: \"Enter system setup to check hardware detection and boot order\",\n imageUrl: \"/images/steps/bios-access.svg\",\n tips: [\"Common keys: F2, F12, Del, Esc during startup\"]\n },\n {\n stepNumber: 3,\n title: \"Try safe boot options\",\n description: \"Attempt to boot using safe mode or last known good configuration\",\n imageUrl: \"/images/steps/safe-boot.svg\",\n tips: [\"Use F8 key during boot for advanced options\"]\n },\n {\n stepNumber: 4,\n title: \"Run startup repair\",\n description: \"Use Windows recovery tools to fix boot-related issues\",\n imageUrl: \"/images/steps/startup-repair.svg\",\n tips: [\"Boot from Windows installation media if needed\"]\n },\n {\n stepNumber: 5,\n title: \"Check hard drive\",\n description: \"Verify hard drive health and file system integrity\",\n imageUrl: \"/images/steps/hdd-check.svg\",\n tips: [\"Use command prompt to run chkdsk /f\"]\n }\n ],\n alternativeSolutions: \"If hardware failure suspected, professional diagnosis recommended\",\n imageUrl: \"/images/guides/boot-repair.svg\"\n },\n {\n title: \"Restore Corrupted System Files\",\n description: \"Use Windows built-in tools to repair corrupted or missing system files.\",\n deviceType: \"laptop\",\n category: \"software\",\n difficulty: \"medium\",\n estimatedTime: \"60-90 min\",\n toolsRequired: [\"Administrative access\", \"Windows installation media\", \"Internet connection\"],\n safetyWarnings: [\n \"Create system restore point before starting\",\n \"Ensure stable power during repair process\",\n \"Have Windows installation media available\"\n ],\n steps: [\n {\n stepNumber: 1,\n title: \"Run System File Checker\",\n description: \"Use SFC command to scan and repair system files\",\n imageUrl: \"/images/steps/sfc-scan.svg\",\n tips: [\"Run 'sfc /scannow' in elevated command prompt\"]\n },\n {\n stepNumber: 2,\n title: \"Use DISM tool\",\n description: \"Run Deployment Image Servicing tool to repair Windows image\",\n imageUrl: \"/images/steps/dism-repair.svg\",\n tips: [\"Use 'DISM /Online /Cleanup-Image /RestoreHealth'\"]\n },\n {\n stepNumber: 3,\n title: \"Check Windows Update\",\n description: \"Install latest updates to replace corrupted files\",\n imageUrl: \"/images/steps/windows-update-repair.svg\",\n tips: [\"Some corrupted files are fixed by updates\"]\n },\n {\n stepNumber: 4,\n title: \"Reset Windows components\",\n description: \"Reset specific Windows components if issues persist\",\n imageUrl: \"/images/steps/component-reset.svg\",\n tips: [\"Focus on problematic components like Windows Store\"]\n }\n ],\n alternativeSolutions: \"Consider in-place Windows upgrade if corruption is extensive\",\n imageUrl: \"/images/guides/system-file-repair.svg\"\n },\n {\n title: \"Speed Up Slow Laptop Performance\",\n description: \"Comprehensive optimization to improve laptop speed and responsiveness.\",\n deviceType: \"laptop\",\n category: \"software\",\n difficulty: \"easy\",\n estimatedTime: \"45-60 min\",\n toolsRequired: [\"Disk cleanup tools\", \"Task Manager access\", \"Administrative privileges\"],\n safetyWarnings: [\n \"Create backup before making major changes\",\n \"Don't disable unknown services\",\n \"Monitor system stability after changes\"\n ],\n steps: [\n {\n stepNumber: 1,\n title: \"Check storage space\",\n description: \"Free up disk space using built-in cleanup tools\",\n imageUrl: \"/images/steps/storage-cleanup.svg\",\n tips: [\"Aim for at least 15% free space on system drive\"]\n },\n {\n stepNumber: 2,\n title: \"Manage startup programs\",\n description: \"Disable unnecessary programs from starting with Windows\",\n imageUrl: \"/images/steps/startup-management.svg\",\n tips: [\"Keep only essential programs in startup\"]\n },\n {\n stepNumber: 3,\n title: \"Update drivers\",\n description: \"Install latest drivers for better hardware performance\",\n imageUrl: \"/images/steps/driver-updates.svg\",\n tips: [\"Focus on graphics, storage, and network drivers\"]\n },\n {\n stepNumber: 4,\n title: \"Adjust visual effects\",\n description: \"Optimize Windows visual effects for performance\",\n imageUrl: \"/images/steps/visual-optimization.svg\",\n tips: [\"Choose 'Adjust for best performance' in System Properties\"]\n },\n {\n stepNumber: 5,\n title: \"Scan for malware\",\n description: \"Run thorough malware scan to remove performance-affecting threats\",\n imageUrl: \"/images/steps/performance-scan.svg\",\n tips: [\"Use multiple scanning tools for best results\"]\n }\n ],\n alternativeSolutions: \"Consider hardware upgrades if software optimization isn't sufficient\",\n imageUrl: \"/images/guides/performance-optimization.svg\"\n },\n\n // LAPTOP CLEANING GUIDES\n {\n title: \"Deep Clean Laptop Internals\",\n description: \"Thorough cleaning of laptop internal components to improve cooling and performance.\",\n deviceType: \"laptop\",\n category: \"cleaning\",\n difficulty: \"medium\",\n estimatedTime: \"60-90 min\",\n toolsRequired: [\"Compressed air\", \"Soft brushes\", \"Isopropyl alcohol\", \"Cotton swabs\", \"Thermal paste\"],\n safetyWarnings: [\n \"Ensure laptop is completely powered off and unplugged\",\n \"Use only electronics-safe cleaning materials\",\n \"Allow complete drying before reassembly\"\n ],\n steps: [\n {\n stepNumber: 1,\n title: \"Disassemble laptop\",\n description: \"Remove bottom panel and disconnect battery for safety\",\n imageUrl: \"/images/steps/laptop-disassembly.svg\",\n tips: [\"Take photos during disassembly for reference during reassembly\"]\n },\n {\n stepNumber: 2,\n title: \"Clean fans and vents\",\n description: \"Use compressed air to blow out dust from fans and air vents\",\n imageUrl: \"/images/steps/fan-cleaning.svg\",\n tips: [\"Hold fans stationary while blowing air to prevent overspeeding\"]\n },\n {\n stepNumber: 3,\n title: \"Clean heat sinks\",\n description: \"Remove accumulated dust from CPU and GPU heat sinks\",\n imageUrl: \"/images/steps/heatsink-cleaning.svg\",\n tips: [\"Use soft brush for stubborn dust buildup\"]\n },\n {\n stepNumber: 4,\n title: \"Replace thermal paste\",\n description: \"Clean old thermal paste and apply fresh paste to CPU\",\n imageUrl: \"/images/steps/thermal-paste-replacement.svg\",\n tips: [\"Use high-quality thermal paste and apply thin, even layer\"]\n },\n {\n stepNumber: 5,\n title: \"Clean other components\",\n description: \"Gently clean RAM, storage, and other accessible components\",\n imageUrl: \"/images/steps/component-cleaning.svg\",\n tips: [\"Use dry methods only for electronic components\"]\n }\n ],\n alternativeSolutions: \"For severe overheating, consider professional cleaning service\",\n imageUrl: \"/images/guides/laptop-deep-cleaning.svg\"\n },\n {\n title: \"Clean Laptop Screen and Exterior\",\n description: \"Safely clean laptop screen, keyboard, and exterior surfaces without damage.\",\n deviceType: \"laptop\",\n category: \"cleaning\",\n difficulty: \"easy\",\n estimatedTime: \"15-30 min\",\n toolsRequired: [\"Microfiber cloths\", \"Screen cleaner\", \"Isopropyl alcohol\", \"Cotton swabs\", \"Compressed air\"],\n safetyWarnings: [\n \"Power off laptop before cleaning\",\n \"Never spray liquids directly on screen\",\n \"Use only LCD-safe cleaning solutions\"\n ],\n steps: [\n {\n stepNumber: 1,\n title: \"Prepare cleaning area\",\n description: \"Power off laptop and gather appropriate cleaning materials\",\n imageUrl: \"/images/steps/cleaning-prep.svg\",\n tips: [\"Let laptop cool down if it was recently used\"]\n },\n {\n stepNumber: 2,\n title: \"Clean keyboard\",\n description: \"Use compressed air and damp cloth to clean keys and spaces\",\n imageUrl: \"/images/steps/keyboard-cleaning.svg\",\n tips: [\"Turn laptop upside down and shake gently to remove debris\"]\n },\n {\n stepNumber: 3,\n title: \"Clean screen\",\n description: \"Use microfiber cloth with screen cleaner in circular motions\",\n imageUrl: \"/images/steps/screen-cleaning.svg\",\n tips: [\"Start from center and work outward, avoid excessive pressure\"]\n },\n {\n stepNumber: 4,\n title: \"Clean exterior\",\n description: \"Wipe down laptop exterior, ports, and ventilation areas\",\n imageUrl: \"/images/steps/exterior-cleaning.svg\",\n tips: [\"Use cotton swabs for hard-to-reach port areas\"]\n }\n ],\n alternativeSolutions: \"For stubborn stains, consider professional cleaning services\",\n imageUrl: \"/images/guides/laptop-screen-cleaning.svg\"\n },\n {\n title: \"Remove Liquid Spill Damage\",\n description: \"Emergency cleaning procedures for laptops with liquid spill damage.\",\n deviceType: \"laptop\",\n category: \"cleaning\",\n difficulty: \"hard\",\n estimatedTime: \"120-180 min\",\n toolsRequired: [\"Isopropyl alcohol\", \"Soft brushes\", \"Cotton swabs\", \"Distilled water\", \"Hair dryer\"],\n safetyWarnings: [\n \"Immediately power off and unplug laptop\",\n \"Remove battery if possible\",\n \"Work in well-ventilated area when using alcohol\"\n ],\n steps: [\n {\n stepNumber: 1,\n title: \"Emergency shutdown\",\n description: \"Immediately turn off laptop and remove all power sources\",\n imageUrl: \"/images/steps/emergency-shutdown.svg\",\n tips: [\"Speed is critical - don't wait for proper shutdown\"]\n },\n {\n stepNumber: 2,\n title: \"Disassemble laptop\",\n description: \"Remove back panel, battery, and affected components\",\n imageUrl: \"/images/steps/spill-disassembly.svg\",\n tips: [\"Take photos to remember component placement\"]\n },\n {\n stepNumber: 3,\n title: \"Clean affected areas\",\n description: \"Use isopropyl alcohol to clean corrosion and residue\",\n imageUrl: \"/images/steps/corrosion-cleaning.svg\",\n tips: [\"Use 99% isopropyl alcohol for best results\"]\n },\n {\n stepNumber: 4,\n title: \"Dry thoroughly\",\n description: \"Ensure all components are completely dry before reassembly\",\n imageUrl: \"/images/steps/component-drying.svg\",\n tips: [\"Allow 24-48 hours drying time in dry environment\"]\n },\n {\n stepNumber: 5,\n title: \"Test components\",\n description: \"Reassemble and test each component before full assembly\",\n imageUrl: \"/images/steps/component-testing.svg\",\n tips: [\"Test with minimal components first\"]\n }\n ],\n alternativeSolutions: \"Severe damage may require professional data recovery services\",\n imageUrl: \"/images/guides/liquid-damage-repair.svg\"\n },\n\n // DESKTOP HARDWARE GUIDES\n {\n title: \"Build Custom Desktop PC\",\n description: \"Complete guide to building a desktop computer from individual components.\",\n deviceType: \"desktop\",\n category: \"hardware\",\n difficulty: \"hard\",\n estimatedTime: \"120-180 min\",\n toolsRequired: [\"Phillips head screwdriver\", \"Anti-static wrist strap\", \"Zip ties\", \"Thermal paste\"],\n safetyWarnings: [\n \"Use anti-static protection throughout build process\",\n \"Ensure PSU is switched off and unplugged\",\n \"Handle components by edges and avoid touching circuits\"\n ],\n steps: [\n {\n stepNumber: 1,\n title: \"Prepare motherboard\",\n description: \"Install CPU, RAM, and M.2 storage on motherboard outside of case\",\n imageUrl: \"/images/steps/motherboard-preparation.svg\",\n tips: [\"Install components on flat, anti-static surface\"]\n },\n {\n stepNumber: 2,\n title: \"Install PSU and I/O shield\",\n description: \"Mount power supply and install motherboard I/O shield in case\",\n imageUrl: \"/images/steps/psu-io-installation.svg\",\n tips: [\"Orient PSU fan correctly based on case ventilation\"]\n },\n {\n stepNumber: 3,\n title: \"Mount motherboard\",\n description: \"Secure motherboard in case using proper standoffs\",\n imageUrl: \"/images/steps/motherboard-mounting.svg\",\n tips: [\"Don't overtighten screws - snug fit is sufficient\"]\n },\n {\n stepNumber: 4,\n title: \"Install graphics card\",\n description: \"Secure GPU in PCIe slot and connect power connectors\",\n imageUrl: \"/images/steps/gpu-installation.svg\",\n tips: [\"Ensure card is fully seated and rear bracket is secured\"]\n },\n {\n stepNumber: 5,\n title: \"Connect all cables\",\n description: \"Connect power, data, and front panel connectors\",\n imageUrl: \"/images/steps/cable-connections.svg\",\n tips: [\"Refer to motherboard manual for front panel connector layout\"]\n },\n {\n stepNumber: 6,\n title: \"First boot test\",\n description: \"Power on system and verify all components are detected\",\n imageUrl: \"/images/steps/first-boot-test.svg\",\n tips: [\"Enter BIOS to verify CPU, RAM, and storage are detected\"]\n }\n ],\n alternativeSolutions: \"Consider pre-built systems if building seems too complex\",\n imageUrl: \"/images/guides/desktop-pc-build.svg\"\n },\n {\n title: \"Upgrade Desktop Graphics Card\",\n description: \"Step-by-step guide to replacing or upgrading desktop GPU for better performance.\",\n deviceType: \"desktop\",\n category: \"hardware\",\n difficulty: \"medium\",\n estimatedTime: \"30-45 min\",\n toolsRequired: [\"Phillips head screwdriver\", \"Anti-static wrist strap\"],\n safetyWarnings: [\n \"Power off PC and unplug from wall before starting\",\n \"Ensure new GPU is compatible with PSU wattage\",\n \"Check physical clearance in case\"\n ],\n steps: [\n {\n stepNumber: 1,\n title: \"Uninstall old drivers\",\n description: \"Use DDU (Display Driver Uninstaller) to remove existing graphics drivers\",\n imageUrl: \"/images/steps/driver-uninstall.svg\",\n tips: [\"Boot into safe mode for thorough driver removal\"]\n },\n {\n stepNumber: 2,\n title: \"Remove old GPU\",\n description: \"Disconnect power cables and unscrew rear bracket, then remove from PCIe slot\",\n imageUrl: \"/images/steps/gpu-removal.svg\",\n tips: [\"Press PCIe release tab while gently pulling card\"]\n },\n {\n stepNumber: 3,\n title: \"Install new GPU\",\n description: \"Insert new graphics card into PCIe slot and secure with screws\",\n imageUrl: \"/images/steps/new-gpu-install.svg\",\n tips: [\"Ensure card clicks securely into slot\"]\n },\n {\n stepNumber: 4,\n title: \"Connect power\",\n description: \"Attach required PCIe power connectors from PSU to graphics card\",\n imageUrl: \"/images/steps/gpu-power-connection.svg\",\n tips: [\"Use separate cables for each connector if possible\"]\n },\n {\n stepNumber: 5,\n title: \"Install new drivers\",\n description: \"Download and install latest drivers from manufacturer\",\n imageUrl: \"/images/steps/new-driver-install.svg\",\n tips: [\"Use manufacturer's official website for drivers\"]\n }\n ],\n alternativeSolutions: \"If performance issues persist, consider CPU or PSU bottlenecks\",\n imageUrl: \"/images/guides/desktop-gpu-upgrade.svg\"\n },\n\n // DESKTOP SOFTWARE GUIDES\n {\n title: \"Optimize Windows 11 Performance\",\n description: \"Comprehensive guide to optimize Windows 11 for better speed and responsiveness.\",\n deviceType: \"desktop\",\n category: \"software\",\n difficulty: \"easy\",\n estimatedTime: \"45-60 min\",\n toolsRequired: [\"Administrative access\", \"Disk cleanup tools\"],\n safetyWarnings: [\n \"Create system restore point before making changes\",\n \"Only disable services you understand\",\n \"Backup important data before major changes\"\n ],\n steps: [\n {\n stepNumber: 1,\n title: \"Update Windows\",\n description: \"Install all available Windows updates and driver updates\",\n imageUrl: \"/images/steps/windows-update.svg\",\n tips: [\"Restart between major updates for best results\"]\n },\n {\n stepNumber: 2,\n title: \"Clean disk space\",\n description: \"Use Disk Cleanup and Storage Sense to remove unnecessary files\",\n imageUrl: \"/images/steps/disk-cleanup.svg\",\n tips: [\"Clean system files and temporary folders\"]\n },\n {\n stepNumber: 3,\n title: \"Disable startup programs\",\n description: \"Turn off unnecessary programs that start with Windows\",\n imageUrl: \"/images/steps/startup-programs.svg\",\n tips: [\"Keep only essential programs for faster boot times\"]\n },\n {\n stepNumber: 4,\n title: \"Adjust visual effects\",\n description: \"Optimize visual effects for performance over appearance\",\n imageUrl: \"/images/steps/visual-effects.svg\",\n tips: [\"Custom settings allow fine-tuning of specific effects\"]\n },\n {\n stepNumber: 5,\n title: \"Configure power settings\",\n description: \"Set power plan to High Performance for desktop PCs\",\n imageUrl: \"/images/steps/power-settings.svg\",\n tips: [\"Balanced mode is fine for most users\"]\n }\n ],\n alternativeSolutions: \"Consider SSD upgrade if system is still slow after optimization\",\n imageUrl: \"/images/guides/windows-optimization.svg\"\n },\n\n // DESKTOP CLEANING GUIDES\n {\n title: \"Clean Desktop PC Case and Components\",\n description: \"Thorough cleaning guide for desktop PC case, fans, and internal components.\",\n deviceType: \"desktop\",\n category: \"cleaning\",\n difficulty: \"easy\",\n estimatedTime: \"45-60 min\",\n toolsRequired: [\"Compressed air\", \"Soft brushes\", \"Microfiber cloths\", \"Isopropyl alcohol\"],\n safetyWarnings: [\n \"Power off and unplug PC before cleaning\",\n \"Ground yourself to prevent static discharge\",\n \"Allow complete drying before powering on\"\n ],\n steps: [\n {\n stepNumber: 1,\n title: \"Remove side panels\",\n description: \"Open PC case by removing side panels for access to components\",\n imageUrl: \"/images/steps/case-opening.svg\",\n tips: [\"Some cases have thumbscrews, others require tools\"]\n },\n {\n stepNumber: 2,\n title: \"Clean case fans\",\n description: \"Use compressed air to blow dust from all case fans\",\n imageUrl: \"/images/steps/fan-dust-removal.svg\",\n tips: [\"Hold fan blades stationary while blowing air\"]\n },\n {\n stepNumber: 3,\n title: \"Clean dust filters\",\n description: \"Remove and wash dust filters in warm, soapy water\",\n imageUrl: \"/images/steps/filter-cleaning.svg\",\n tips: [\"Let filters dry completely before reinstalling\"]\n },\n {\n stepNumber: 4,\n title: \"Clean components\",\n description: \"Gently clean GPU, RAM, and other visible components\",\n imageUrl: \"/images/steps/component-dusting.svg\",\n tips: [\"Use soft brush for delicate components\"]\n },\n {\n stepNumber: 5,\n title: \"Clean case exterior\",\n description: \"Wipe down case exterior and ports with damp cloth\",\n imageUrl: \"/images/steps/case-exterior-clean.svg\",\n tips: [\"Use isopropyl alcohol for stubborn stains\"]\n }\n ],\n alternativeSolutions: \"Consider positive pressure case setup to reduce dust accumulation\",\n imageUrl: \"/images/guides/desktop-cleaning.svg\"\n },\n {\n title: \"Deep Clean Gaming PC Setup\",\n description: \"Comprehensive cleaning for gaming PCs including peripherals and cable management.\",\n deviceType: \"desktop\",\n category: \"cleaning\",\n difficulty: \"medium\",\n estimatedTime: \"90-120 min\",\n toolsRequired: [\"Compressed air\", \"Cable management tools\", \"Cleaning solutions\", \"Soft brushes\"],\n safetyWarnings: [\n \"Disconnect all power before starting\",\n \"Take photos of cable arrangements before disconnecting\",\n \"Use appropriate cleaning solutions for different materials\"\n ],\n steps: [\n {\n stepNumber: 1,\n title: \"Document current setup\",\n description: \"Take photos of cable arrangements and component placement\",\n imageUrl: \"/images/steps/setup-documentation.svg\",\n tips: [\"This helps with reassembly and troubleshooting\"]\n },\n {\n stepNumber: 2,\n title: \"Remove components\",\n description: \"Carefully remove GPU, RAM, and other accessible components\",\n imageUrl: \"/images/steps/component-removal.svg\",\n tips: [\"Only remove components you're comfortable reinstalling\"]\n },\n {\n stepNumber: 3,\n title: \"Deep clean case\",\n description: \"Thoroughly clean case interior, fans, and all surfaces\",\n imageUrl: \"/images/steps/deep-case-cleaning.svg\",\n tips: [\"Pay attention to hard-to-reach areas behind motherboard\"]\n },\n {\n stepNumber: 4,\n title: \"Clean and reorganize cables\",\n description: \"Clean cables and improve cable management layout\",\n imageUrl: \"/images/steps/cable-management.svg\",\n tips: [\"Use velcro ties and routing holes for clean appearance\"]\n },\n {\n stepNumber: 5,\n title: \"Clean peripherals\",\n description: \"Clean keyboard, mouse, monitor, and other accessories\",\n imageUrl: \"/images/steps/peripheral-cleaning.svg\",\n tips: [\"Remove keycaps for thorough keyboard cleaning\"]\n },\n {\n stepNumber: 6,\n title: \"Reassemble and test\",\n description: \"Reinstall components and verify system functionality\",\n imageUrl: \"/images/steps/system-reassembly.svg\",\n tips: [\"Test each component as you reinstall it\"]\n }\n ],\n alternativeSolutions: \"Professional cleaning services available for complex builds\",\n imageUrl: \"/images/guides/gaming-pc-deep-clean.svg\"\n },\n {\n title: \"Clean and Maintain Water Cooling Loop\",\n description: \"Maintenance guide for custom water cooling systems including cleaning and fluid replacement.\",\n deviceType: \"desktop\",\n category: \"cleaning\",\n difficulty: \"hard\",\n estimatedTime: \"180-240 min\",\n toolsRequired: [\"Distilled water\", \"Coolant\", \"Soft tubing brush\", \"Fittings\", \"Towels\"],\n safetyWarnings: [\n \"Completely drain system before disassembly\",\n \"Use only appropriate coolants and cleaning solutions\",\n \"Check for leaks thoroughly before powering on\"\n ],\n steps: [\n {\n stepNumber: 1,\n title: \"Drain cooling system\",\n description: \"Safely drain all coolant from the water cooling loop\",\n imageUrl: \"/images/steps/loop-draining.svg\",\n tips: [\"Use drain valve or remove lowest fitting to empty system\"]\n },\n {\n stepNumber: 2,\n title: \"Disassemble loop\",\n description: \"Carefully disconnect fittings and remove components\",\n imageUrl: \"/images/steps/loop-disassembly.svg\",\n tips: [\"Label connections and take photos for reference\"]\n },\n {\n stepNumber: 3,\n title: \"Clean radiators\",\n description: \"Flush radiators with distilled water and cleaning solution\",\n imageUrl: \"/images/steps/radiator-cleaning.svg\",\n tips: [\"Reverse flush direction multiple times for best results\"]\n },\n {\n stepNumber: 4,\n title: \"Clean water blocks\",\n description: \"Disassemble and clean CPU and GPU water blocks\",\n imageUrl: \"/images/steps/waterblock-cleaning.svg\",\n tips: [\"Use soft brush to remove buildup from micro-fins\"]\n },\n {\n stepNumber: 5,\n title: \"Replace tubing and fittings\",\n description: \"Install new soft tubing and check all fittings\",\n imageUrl: \"/images/steps/tubing-replacement.svg\",\n tips: [\"Replace tubing annually for best performance\"]\n },\n {\n stepNumber: 6,\n title: \"Refill and test\",\n description: \"Refill system with fresh coolant and test for leaks\",\n imageUrl: \"/images/steps/loop-refilling.svg\",\n tips: [\"Run pump without power to components during initial fill\"]\n }\n ],\n alternativeSolutions: \"Consider AIO coolers if custom loop maintenance is too complex\",\n imageUrl: \"/images/guides/watercooling-maintenance.svg\"\n },\n\n // UPGRADE GUIDES FOR BOTH TYPES\n {\n title: \"Upgrade to Windows 11\",\n description: \"Complete guide to upgrading from Windows 10 to Windows 11 safely.\",\n deviceType: \"laptop\",\n category: \"upgrades\",\n difficulty: \"medium\",\n estimatedTime: \"90-120 min\",\n toolsRequired: [\"Reliable internet connection\", \"8GB+ USB drive\", \"Windows 11 installation media\"],\n safetyWarnings: [\n \"Backup all important data before upgrading\",\n \"Ensure hardware meets Windows 11 requirements\",\n \"Have recovery media ready in case of issues\"\n ],\n steps: [\n {\n stepNumber: 1,\n title: \"Check compatibility\",\n description: \"Run PC Health Check tool to verify Windows 11 compatibility\",\n imageUrl: \"/images/steps/compatibility-check.svg\",\n tips: [\"Enable TPM 2.0 and Secure Boot if required\"]\n },\n {\n stepNumber: 2,\n title: \"Backup data\",\n description: \"Create complete backup of important files and system\",\n imageUrl: \"/images/steps/data-backup-upgrade.svg\",\n tips: [\"Use Windows backup tool or third-party solution\"]\n },\n {\n stepNumber: 3,\n title: \"Update drivers\",\n description: \"Install latest drivers and Windows updates before upgrading\",\n imageUrl: \"/images/steps/pre-upgrade-updates.svg\",\n tips: [\"Focus on critical drivers like graphics and storage\"]\n },\n {\n stepNumber: 4,\n title: \"Start upgrade\",\n description: \"Run Windows 11 installation or use Windows Update\",\n imageUrl: \"/images/steps/windows11-upgrade.svg\",\n tips: [\"Choose to keep files and apps during upgrade\"]\n },\n {\n stepNumber: 5,\n title: \"Post-upgrade setup\",\n description: \"Configure Windows 11 settings and verify all programs work\",\n imageUrl: \"/images/steps/post-upgrade-setup.svg\",\n tips: [\"Check Device Manager for any driver issues\"]\n }\n ],\n alternativeSolutions: \"Consider clean installation if upgrade causes issues\",\n imageUrl: \"/images/guides/windows11-upgrade.svg\"\n },\n {\n title: \"Upgrade Laptop to SSD Storage\",\n description: \"Complete guide to upgrading from HDD to SSD for dramatic performance improvement.\",\n deviceType: \"laptop\",\n category: \"upgrades\",\n difficulty: \"medium\",\n estimatedTime: \"90-120 min\",\n toolsRequired: [\"SSD drive\", \"SATA to USB adapter\", \"Cloning software\", \"Phillips screwdriver\"],\n safetyWarnings: [\n \"Backup all data before starting upgrade\",\n \"Ensure SSD is compatible with laptop interface\",\n \"Handle storage devices carefully to prevent damage\"\n ],\n steps: [\n {\n stepNumber: 1,\n title: \"Choose right SSD\",\n description: \"Select appropriate SSD size and interface for your laptop\",\n imageUrl: \"/images/steps/ssd-selection.svg\",\n tips: [\"Check current drive interface - SATA, M.2, or NVMe\"]\n },\n {\n stepNumber: 2,\n title: \"Clone existing drive\",\n description: \"Use cloning software to copy all data to new SSD\",\n imageUrl: \"/images/steps/drive-cloning.svg\",\n tips: [\"Use manufacturer cloning tools when available\"]\n },\n {\n stepNumber: 3,\n title: \"Install SSD\",\n description: \"Replace old drive with new SSD in laptop\",\n imageUrl: \"/images/steps/ssd-physical-install.svg\",\n tips: [\"Note cable orientation and mounting screw locations\"]\n },\n {\n stepNumber: 4,\n title: \"First boot test\",\n description: \"Boot from new SSD and verify system functionality\",\n imageUrl: \"/images/steps/ssd-first-boot.svg\",\n tips: [\"Check that all programs and files are accessible\"]\n },\n {\n stepNumber: 5,\n title: \"Optimize SSD settings\",\n description: \"Configure Windows settings for optimal SSD performance\",\n imageUrl: \"/images/steps/ssd-optimization.svg\",\n tips: [\"Enable TRIM, disable defragmentation for SSD\"]\n }\n ],\n alternativeSolutions: \"Keep old HDD as external storage using USB enclosure\",\n imageUrl: \"/images/guides/ssd-upgrade.svg\"\n },\n {\n title: \"Upgrade Laptop RAM Memory\",\n description: \"Step-by-step guide to increasing laptop memory for better multitasking performance.\",\n deviceType: \"laptop\",\n category: \"upgrades\",\n difficulty: \"easy\",\n estimatedTime: \"30-45 min\",\n toolsRequired: [\"Compatible RAM modules\", \"Phillips screwdriver\", \"Anti-static wrist strap\"],\n safetyWarnings: [\n \"Check maximum RAM capacity and speed supported\",\n \"Use anti-static protection when handling memory\",\n \"Power off laptop completely before installation\"\n ],\n steps: [\n {\n stepNumber: 1,\n title: \"Check current RAM\",\n description: \"Identify current memory configuration and available slots\",\n imageUrl: \"/images/steps/ram-identification.svg\",\n tips: [\"Use System Information or CPU-Z to check current setup\"]\n },\n {\n stepNumber: 2,\n title: \"Purchase compatible RAM\",\n description: \"Buy memory modules matching laptop specifications\",\n imageUrl: \"/images/steps/ram-purchasing.svg\",\n tips: [\"Match speed, voltage, and form factor of existing RAM\"]\n },\n {\n stepNumber: 3,\n title: \"Install new memory\",\n description: \"Add or replace memory modules in laptop\",\n imageUrl: \"/images/steps/ram-physical-install.svg\",\n tips: [\"Insert at 45-degree angle and press down until clips engage\"]\n },\n {\n stepNumber: 4,\n title: \"Verify installation\",\n description: \"Boot laptop and confirm new memory is recognized\",\n imageUrl: \"/images/steps/ram-verification.svg\",\n tips: [\"Check System Properties to confirm total memory\"]\n }\n ],\n alternativeSolutions: \"Consider external storage if internal upgrades aren't possible\",\n imageUrl: \"/images/guides/ram-upgrade.svg\"\n },\n {\n title: \"Upgrade to Dual Monitor Setup\",\n description: \"Set up external monitor with laptop for improved productivity.\",\n deviceType: \"laptop\",\n category: \"upgrades\",\n difficulty: \"easy\",\n estimatedTime: \"20-30 min\",\n toolsRequired: [\"External monitor\", \"Video cable\", \"Docking station (optional)\"],\n safetyWarnings: [\n \"Check laptop video output compatibility\",\n \"Ensure monitor resolution is supported\",\n \"Use appropriate cables for best quality\"\n ],\n steps: [\n {\n stepNumber: 1,\n title: \"Choose monitor and cable\",\n description: \"Select compatible monitor and connection type\",\n imageUrl: \"/images/steps/monitor-selection.svg\",\n tips: [\"HDMI, USB-C, or DisplayPort are common options\"]\n },\n {\n stepNumber: 2,\n title: \"Connect physical cables\",\n description: \"Connect monitor to laptop using appropriate cable\",\n imageUrl: \"/images/steps/monitor-connection.svg\",\n tips: [\"Ensure secure connections on both ends\"]\n },\n {\n stepNumber: 3,\n title: \"Configure display settings\",\n description: \"Set up display arrangement and resolution in Windows\",\n imageUrl: \"/images/steps/display-configuration.svg\",\n tips: [\"Use Windows + P for quick display mode selection\"]\n },\n {\n stepNumber: 4,\n title: \"Optimize workspace\",\n description: \"Arrange windows and set up taskbar for dual monitor use\",\n imageUrl: \"/images/steps/workspace-optimization.svg\",\n tips: [\"Consider using virtual desktops for better organization\"]\n }\n ],\n alternativeSolutions: \"Wireless display adapters available for cable-free setups\",\n imageUrl: \"/images/guides/dual-monitor-setup.svg\"\n },\n {\n title: \"Upgrade Desktop Graphics Card\",\n description: \"Complete guide to installing a new graphics card for gaming and professional work.\",\n deviceType: \"desktop\",\n category: \"upgrades\",\n difficulty: \"medium\",\n estimatedTime: \"45-60 min\",\n toolsRequired: [\"New graphics card\", \"Phillips screwdriver\", \"PCIe power cables\"],\n safetyWarnings: [\n \"Check PSU wattage requirements for new card\",\n \"Verify physical clearance in case\",\n \"Uninstall old drivers before hardware change\"\n ],\n steps: [\n {\n stepNumber: 1,\n title: \"Choose compatible GPU\",\n description: \"Select graphics card compatible with motherboard and PSU\",\n imageUrl: \"/images/steps/gpu-selection.svg\",\n tips: [\"Check PCIe slot version and power requirements\"]\n },\n {\n stepNumber: 2,\n title: \"Prepare system\",\n description: \"Uninstall old graphics drivers and power down system\",\n imageUrl: \"/images/steps/gpu-preparation.svg\",\n tips: [\"Use DDU (Display Driver Uninstaller) for clean removal\"]\n },\n {\n stepNumber: 3,\n title: \"Install new card\",\n description: \"Mount graphics card in PCIe slot and connect power\",\n imageUrl: \"/images/steps/gpu-installation-desktop.svg\",\n tips: [\"Press firmly until card clicks into place\"]\n },\n {\n stepNumber: 4,\n title: \"Install new drivers\",\n description: \"Download and install latest drivers from manufacturer\",\n imageUrl: \"/images/steps/gpu-driver-install.svg\",\n tips: [\"Get drivers from NVIDIA, AMD, or Intel websites\"]\n },\n {\n stepNumber: 5,\n title: \"Test performance\",\n description: \"Run benchmarks and games to verify proper installation\",\n imageUrl: \"/images/steps/gpu-testing.svg\",\n tips: [\"Monitor temperatures during stress testing\"]\n }\n ],\n alternativeSolutions: \"Consider external GPU enclosures for laptops\",\n imageUrl: \"/images/guides/gpu-upgrade-desktop.svg\"\n },\n {\n title: \"Upgrade to NVMe M.2 SSD\",\n description: \"Install ultra-fast NVMe M.2 SSD for maximum storage performance.\",\n deviceType: \"desktop\",\n category: \"upgrades\",\n difficulty: \"easy\",\n estimatedTime: \"20-30 min\",\n toolsRequired: [\"NVMe M.2 SSD\", \"Small screwdriver\", \"Thermal pad (optional)\"],\n safetyWarnings: [\n \"Check motherboard M.2 slot compatibility\",\n \"Handle SSD carefully - avoid touching contacts\",\n \"Some slots may disable SATA ports when used\"\n ],\n steps: [\n {\n stepNumber: 1,\n title: \"Locate M.2 slot\",\n description: \"Find available M.2 slot on motherboard\",\n imageUrl: \"/images/steps/m2-slot-location.svg\",\n tips: [\"Check motherboard manual for slot specifications\"]\n },\n {\n stepNumber: 2,\n title: \"Install SSD\",\n description: \"Insert M.2 SSD at 30-degree angle and secure with screw\",\n imageUrl: \"/images/steps/m2-installation.svg\",\n tips: [\"Don't force insertion - SSD should slide in easily\"]\n },\n {\n stepNumber: 3,\n title: \"Configure in BIOS\",\n description: \"Enable M.2 slot and set boot priority if needed\",\n imageUrl: \"/images/steps/m2-bios-config.svg\",\n tips: [\"Some motherboards require enabling M.2 in BIOS\"]\n },\n {\n stepNumber: 4,\n title: \"Initialize drive\",\n description: \"Format and partition new SSD in Windows Disk Management\",\n imageUrl: \"/images/steps/m2-initialization.svg\",\n tips: [\"Choose GPT partition style for drives over 2TB\"]\n }\n ],\n alternativeSolutions: \"PCIe to M.2 adapters available for older motherboards\",\n imageUrl: \"/images/guides/nvme-upgrade.svg\"\n }\n];\n\nconst comprehensiveTroubleshootingFlows = [\n {\n title: \"Computer Won't Turn On\",\n description: \"Systematic troubleshooting for computers that won't power on\",\n deviceType: \"both\",\n category: \"hardware\",\n difficulty: \"medium\",\n symptoms: [\"No power\", \"No lights\", \"No fan noise\", \"Dead system\"],\n steps: [\n {\n stepNumber: 1,\n title: \"Check power source\",\n description: \"Verify power outlet and cable connections\",\n possibleCauses: [\"Faulty outlet\", \"Loose power cable\", \"Bad power adapter\"],\n solutions: [\"Test outlet with another device\", \"Reseat all power connections\", \"Try different power adapter\"]\n },\n {\n stepNumber: 2,\n title: \"Check battery (laptops)\",\n description: \"Test if battery is completely drained or faulty\",\n possibleCauses: [\"Dead battery\", \"Faulty battery\", \"Charging port issues\"],\n solutions: [\"Charge for 30+ minutes\", \"Try with adapter only\", \"Check charging port for damage\"]\n },\n {\n stepNumber: 3,\n title: \"Hardware reset\",\n description: \"Perform hard reset to clear any power issues\",\n possibleCauses: [\"Power state corruption\", \"Static buildup\"],\n solutions: [\"Hold power button 30+ seconds\", \"Remove battery and hold power\", \"Reseat RAM modules\"]\n }\n ]\n },\n {\n title: \"Slow Computer Performance\",\n description: \"Diagnose and fix slow computer performance issues\",\n deviceType: \"both\",\n category: \"software\",\n difficulty: \"easy\",\n symptoms: [\"Slow startup\", \"Programs lag\", \"General slowness\", \"Long loading times\"],\n steps: [\n {\n stepNumber: 1,\n title: \"Check available storage\",\n description: \"Verify sufficient free disk space\",\n possibleCauses: [\"Full hard drive\", \"Fragmented disk\", \"Too many files\"],\n solutions: [\"Delete unnecessary files\", \"Use Disk Cleanup\", \"Move files to external storage\"]\n },\n {\n stepNumber: 2,\n title: \"Review startup programs\",\n description: \"Check for too many programs starting with system\",\n possibleCauses: [\"Too many startup programs\", \"Resource-heavy software\"],\n solutions: [\"Disable unnecessary startup programs\", \"Uninstall unused software\", \"Use Task Manager\"]\n },\n {\n stepNumber: 3,\n title: \"Scan for malware\",\n description: \"Check for viruses and malware affecting performance\",\n possibleCauses: [\"Virus infection\", \"Malware\", \"Adware\"],\n solutions: [\"Run full antivirus scan\", \"Use anti-malware tools\", \"Update security software\"]\n }\n ]\n },\n {\n title: \"Internet Connection Problems\",\n description: \"Troubleshoot network and internet connectivity issues\",\n deviceType: \"both\",\n category: \"software\",\n difficulty: \"easy\",\n symptoms: [\"No internet\", \"Slow internet\", \"Intermittent connection\", \"Can't connect to Wi-Fi\"],\n steps: [\n {\n stepNumber: 1,\n title: \"Check physical connections\",\n description: \"Verify all cables and wireless connections\",\n possibleCauses: [\"Loose cables\", \"Disabled Wi-Fi\", \"Router issues\"],\n solutions: [\"Check Ethernet cables\", \"Ensure Wi-Fi is enabled\", \"Restart router/modem\"]\n },\n {\n stepNumber: 2,\n title: \"Reset network settings\",\n description: \"Reset network adapters and configurations\",\n possibleCauses: [\"Corrupted network settings\", \"Wrong IP configuration\"],\n solutions: [\"Run network troubleshooter\", \"Reset TCP/IP stack\", \"Flush DNS cache\"]\n },\n {\n stepNumber: 3,\n title: \"Update network drivers\",\n description: \"Ensure network drivers are current\",\n possibleCauses: [\"Outdated drivers\", \"Corrupted drivers\"],\n solutions: [\"Update network adapter drivers\", \"Reinstall network drivers\", \"Use manufacturer drivers\"]\n }\n ]\n }\n];\n\nconst comprehensiveDeviceComponents = [\n // Laptop Components\n {\n name: \"Laptop CPU\",\n description: \"Central Processing Unit - the main processor of the laptop\",\n deviceType: \"laptop\",\n componentType: \"processor\",\n commonIssues: [\"Overheating\", \"Performance degradation\", \"Complete failure\"],\n replacementDifficulty: \"hard\",\n averageCost: \"$200-800\",\n toolsRequired: [\"Advanced tools\", \"Thermal paste\", \"Heat gun\"],\n imageUrl: \"/images/components/laptop-cpu.svg\"\n },\n {\n name: \"Laptop RAM\",\n description: \"Random Access Memory modules for temporary data storage\",\n deviceType: \"laptop\",\n componentType: \"memory\",\n commonIssues: [\"System crashes\", \"Memory errors\", \"Slow performance\"],\n replacementDifficulty: \"easy\",\n averageCost: \"$50-200\",\n toolsRequired: [\"Phillips screwdriver\", \"Anti-static wrist strap\"],\n imageUrl: \"/images/components/laptop-ram.svg\"\n },\n {\n name: \"Laptop Battery\",\n description: \"Rechargeable battery providing portable power\",\n deviceType: \"laptop\",\n componentType: \"power\",\n commonIssues: [\"Won't hold charge\", \"Swollen battery\", \"Not charging\"],\n replacementDifficulty: \"easy\",\n averageCost: \"$50-150\",\n toolsRequired: [\"Phillips screwdriver\"],\n imageUrl: \"/images/components/laptop-battery.svg\"\n },\n {\n name: \"Laptop Hard Drive\",\n description: \"Storage device for operating system and user data\",\n deviceType: \"laptop\",\n componentType: \"storage\",\n commonIssues: [\"Clicking sounds\", \"Slow performance\", \"Boot failures\"],\n replacementDifficulty: \"medium\",\n averageCost: \"$50-300\",\n toolsRequired: [\"Phillips screwdriver\", \"SATA cable\"],\n imageUrl: \"/images/components/laptop-hdd.svg\"\n },\n {\n name: \"Laptop Screen\",\n description: \"LCD or LED display panel\",\n deviceType: \"laptop\",\n componentType: \"display\",\n commonIssues: [\"Cracked screen\", \"Dead pixels\", \"Flickering\", \"No display\"],\n replacementDifficulty: \"hard\",\n averageCost: \"$100-400\",\n toolsRequired: [\"Phillips screwdriver\", \"Plastic prying tools\"],\n imageUrl: \"/images/components/laptop-screen.svg\"\n },\n {\n name: \"Laptop Keyboard\",\n description: \"Input device for typing and system control\",\n deviceType: \"laptop\",\n componentType: \"input\",\n commonIssues: [\"Stuck keys\", \"Non-responsive keys\", \"Liquid damage\"],\n replacementDifficulty: \"medium\",\n averageCost: \"$30-100\",\n toolsRequired: [\"Phillips screwdriver\", \"Plastic tools\"],\n imageUrl: \"/images/components/laptop-keyboard.svg\"\n },\n {\n name: \"Laptop Trackpad\",\n description: \"Touch-sensitive pointing device\",\n deviceType: \"laptop\",\n componentType: \"input\",\n commonIssues: [\"Erratic cursor movement\", \"Non-responsive\", \"Clicking issues\"],\n replacementDifficulty: \"medium\",\n averageCost: \"$25-75\",\n toolsRequired: [\"Phillips screwdriver\", \"Ribbon cable tools\"],\n imageUrl: \"/images/components/laptop-trackpad.svg\"\n },\n {\n name: \"Laptop Wi-Fi Card\",\n description: \"Wireless network adapter for internet connectivity\",\n deviceType: \"laptop\",\n componentType: \"networking\",\n commonIssues: [\"No Wi-Fi detected\", \"Slow speeds\", \"Frequent disconnections\"],\n replacementDifficulty: \"easy\",\n averageCost: \"$20-60\",\n toolsRequired: [\"Phillips screwdriver\"],\n imageUrl: \"/images/components/laptop-wifi.svg\"\n },\n\n // Desktop Components\n {\n name: \"Desktop CPU\",\n description: \"Central Processing Unit for desktop computers\",\n deviceType: \"desktop\",\n componentType: \"processor\",\n commonIssues: [\"Overheating\", \"Performance bottlenecks\", \"Socket damage\"],\n replacementDifficulty: \"medium\",\n averageCost: \"$100-1000\",\n toolsRequired: [\"Thermal paste\", \"CPU cooler\"],\n imageUrl: \"/images/components/desktop-cpu.svg\"\n },\n {\n name: \"Desktop RAM\",\n description: \"Desktop memory modules (DIMM)\",\n deviceType: \"desktop\",\n componentType: \"memory\",\n commonIssues: [\"Blue screens\", \"Memory errors\", \"System instability\"],\n replacementDifficulty: \"easy\",\n averageCost: \"$30-300\",\n toolsRequired: [\"None - tool-free installation\"],\n imageUrl: \"/images/components/desktop-ram.svg\"\n },\n {\n name: \"Graphics Card\",\n description: \"Dedicated GPU for gaming and graphics processing\",\n deviceType: \"desktop\",\n componentType: \"graphics\",\n commonIssues: [\"Artifacts on screen\", \"Overheating\", \"Driver crashes\"],\n replacementDifficulty: \"easy\",\n averageCost: \"$150-2000\",\n toolsRequired: [\"Phillips screwdriver\", \"PCIe power cables\"],\n imageUrl: \"/images/components/desktop-gpu.svg\"\n },\n {\n name: \"Power Supply\",\n description: \"Converts AC power to DC power for computer components\",\n deviceType: \"desktop\",\n componentType: \"power\",\n commonIssues: [\"No power\", \"Shutdowns under load\", \"Loud fan noise\"],\n replacementDifficulty: \"medium\",\n averageCost: \"$50-300\",\n toolsRequired: [\"Phillips screwdriver\", \"Cable management\"],\n imageUrl: \"/images/components/desktop-psu.svg\"\n },\n {\n name: \"Motherboard\",\n description: \"Main circuit board connecting all components\",\n deviceType: \"desktop\",\n componentType: \"mainboard\",\n commonIssues: [\"Won't boot\", \"Component not detected\", \"Port failures\"],\n replacementDifficulty: \"hard\",\n averageCost: \"$100-500\",\n toolsRequired: [\"Full tool kit\", \"Thermal paste\", \"Cable management\"],\n imageUrl: \"/images/components/desktop-motherboard.svg\"\n },\n {\n name: \"Desktop Hard Drive\",\n description: \"Traditional spinning disk storage\",\n deviceType: \"desktop\",\n componentType: \"storage\",\n commonIssues: [\"Clicking noises\", \"Slow performance\", \"Bad sectors\"],\n replacementDifficulty: \"easy\",\n averageCost: \"$50-200\",\n toolsRequired: [\"SATA cables\", \"Phillips screwdriver\"],\n imageUrl: \"/images/components/desktop-hdd.svg\"\n },\n {\n name: \"SSD (Solid State Drive)\",\n description: \"Fast flash-based storage device\",\n deviceType: \"desktop\",\n componentType: \"storage\",\n commonIssues: [\"Sudden failure\", \"Performance degradation\", \"Not detected\"],\n replacementDifficulty: \"easy\",\n averageCost: \"$50-400\",\n toolsRequired: [\"SATA cables\", \"Phillips screwdriver\"],\n imageUrl: \"/images/components/desktop-ssd.svg\"\n }\n];\n\nexport async function seedDatabase() {\n try {\n console.log(\"Seeding repair guides...\");\n for (const guide of comprehensiveRepairGuides) {\n await db.insert(repairGuides).values({\n ...guide,\n viewCount: Math.floor(Math.random() * 1000),\n isBookmarked: Math.random() > 0.8\n }).onConflictDoNothing();\n }\n\n console.log(\"Seeding troubleshooting flows...\");\n for (const flow of comprehensiveTroubleshootingFlows) {\n const { category, ...flowData } = flow;\n await db.insert(troubleshootingFlows).values({\n ...flowData,\n type: category, // Map category to type field\n steps: flowData.steps.map(step => ({\n id: `step-${step.stepNumber}`,\n question: step.description,\n answers: [{\n text: step.solutions?.join(', ') || \"Check solutions\",\n nextStepId: step.stepNumber < flowData.steps.length ? `step-${step.stepNumber + 1}` : undefined\n }]\n }))\n }).onConflictDoNothing();\n }\n\n console.log(\"Seeding device components...\");\n for (const component of comprehensiveDeviceComponents) {\n const { componentType, commonIssues, ...componentData } = component;\n await db.insert(deviceComponents).values({\n ...componentData,\n category: componentType, // Map componentType to category\n safetyNotes: commonIssues // Map commonIssues to safetyNotes\n }).onConflictDoNothing();\n }\n\n console.log(\"Database seeding completed successfully!\");\n } catch (error) {\n console.error(\"Error seeding database:\", error);\n throw error;\n }\n}","size_bytes":61727},"server/storage.ts":{"content":"import {\n repairGuides,\n troubleshootingFlows,\n deviceComponents,\n users,\n favorites,\n deviceBrands,\n deviceModels,\n guideCompatibility,\n type RepairGuide,\n type InsertRepairGuide,\n type TroubleshootingFlow,\n type InsertTroubleshootingFlow,\n type DeviceComponent,\n type InsertDeviceComponent,\n type User,\n type UpsertUser,\n type Favorite,\n type InsertFavorite,\n type DeviceBrand,\n type InsertDeviceBrand,\n type DeviceModel,\n type InsertDeviceModel,\n type GuideCompatibility,\n type InsertGuideCompatibility,\n} from \"@shared/schema\";\nimport { db } from \"./db\";\nimport { eq, and, like, or, sql } from \"drizzle-orm\";\n\nexport interface IStorage {\n // User operations (mandatory for Replit Auth)\n getUser(id: string): Promise<User | undefined>;\n upsertUser(user: UpsertUser): Promise<User>;\n\n // Favorites operations\n getUserFavorites(userId: string): Promise<Favorite[]>;\n addToFavorites(userId: string, favorite: InsertFavorite): Promise<Favorite>;\n removeFromFavorites(userId: string, itemId: string, itemType: string): Promise<void>;\n isFavorited(userId: string, itemId: string, itemType: string): Promise<boolean>;\n\n // Repair Guides\n getRepairGuides(filters?: { deviceType?: string; category?: string; difficulty?: string }): Promise<RepairGuide[]>;\n getRepairGuide(id: string): Promise<RepairGuide | undefined>;\n createRepairGuide(guide: InsertRepairGuide): Promise<RepairGuide>;\n updateViewCount(id: string): Promise<void>;\n updateDownloadCount(id: string): Promise<void>;\n toggleBookmark(id: string): Promise<RepairGuide | undefined>;\n searchRepairGuides(query: string): Promise<RepairGuide[]>;\n\n // Troubleshooting Flows\n getTroubleshootingFlows(): Promise<TroubleshootingFlow[]>;\n getTroubleshootingFlow(id: string): Promise<TroubleshootingFlow | undefined>;\n createTroubleshootingFlow(flow: InsertTroubleshootingFlow): Promise<TroubleshootingFlow>;\n\n // Device Components\n getDeviceComponents(deviceType?: string): Promise<DeviceComponent[]>;\n getDeviceComponent(id: string): Promise<DeviceComponent | undefined>;\n createDeviceComponent(component: InsertDeviceComponent): Promise<DeviceComponent>;\n\n // Device Brands and Models\n getDeviceBrands(deviceType?: string): Promise<DeviceBrand[]>;\n getDeviceModels(brandId?: string, deviceType?: string): Promise<DeviceModel[]>;\n getDeviceModel(id: string): Promise<DeviceModel | undefined>;\n getModelsByBrand(brandId: string): Promise<DeviceModel[]>;\n searchDeviceModels(query: string, deviceType?: string): Promise<DeviceModel[]>;\n \n // Guide Compatibility\n getCompatibleGuides(modelId: string): Promise<RepairGuide[]>;\n getModelCompatibility(guideId: string): Promise<DeviceModel[]>;\n \n // User guide creation\n createUserGuide(guide: InsertRepairGuide): Promise<RepairGuide>;\n}\n\nexport class DatabaseStorage implements IStorage {\n // User operations (mandatory for Replit Auth)\n async getUser(id: string): Promise<User | undefined> {\n const [user] = await db.select().from(users).where(eq(users.id, id));\n return user;\n }\n\n async upsertUser(userData: UpsertUser): Promise<User> {\n const [user] = await db\n .insert(users)\n .values(userData)\n .onConflictDoUpdate({\n target: users.id,\n set: {\n ...userData,\n updatedAt: new Date(),\n },\n })\n .returning();\n return user;\n }\n\n // Favorites operations\n async getUserFavorites(userId: string): Promise<Favorite[]> {\n return await db.select().from(favorites).where(eq(favorites.userId, userId));\n }\n\n async addToFavorites(userId: string, favorite: InsertFavorite): Promise<Favorite> {\n const [newFavorite] = await db\n .insert(favorites)\n .values({ ...favorite, userId })\n .returning();\n return newFavorite;\n }\n\n async removeFromFavorites(userId: string, itemId: string, itemType: string): Promise<void> {\n await db\n .delete(favorites)\n .where(\n and(\n eq(favorites.userId, userId),\n eq(favorites.itemId, itemId),\n eq(favorites.itemType, itemType)\n )\n );\n }\n\n async isFavorited(userId: string, itemId: string, itemType: string): Promise<boolean> {\n const [favorite] = await db\n .select()\n .from(favorites)\n .where(\n and(\n eq(favorites.userId, userId),\n eq(favorites.itemId, itemId),\n eq(favorites.itemType, itemType)\n )\n );\n return !!favorite;\n }\n\n // Repair Guides operations\n async getRepairGuides(filters?: { deviceType?: string; category?: string; difficulty?: string }): Promise<RepairGuide[]> {\n let query = db.select().from(repairGuides);\n \n if (filters?.deviceType) {\n query = query.where(eq(repairGuides.deviceType, filters.deviceType));\n }\n if (filters?.category) {\n query = query.where(eq(repairGuides.category, filters.category));\n }\n if (filters?.difficulty) {\n query = query.where(eq(repairGuides.difficulty, filters.difficulty));\n }\n \n return await query;\n }\n\n async getRepairGuide(id: string): Promise<RepairGuide | undefined> {\n const [guide] = await db.select().from(repairGuides).where(eq(repairGuides.id, id));\n return guide;\n }\n\n async createRepairGuide(guide: InsertRepairGuide): Promise<RepairGuide> {\n const [newGuide] = await db.insert(repairGuides).values(guide).returning();\n return newGuide;\n }\n\n async updateViewCount(id: string): Promise<void> {\n await db\n .update(repairGuides)\n .set({ viewCount: sql`${repairGuides.viewCount} + 1` })\n .where(eq(repairGuides.id, id));\n }\n\n async updateDownloadCount(id: string): Promise<void> {\n await db\n .update(repairGuides)\n .set({ downloadCount: sql`${repairGuides.downloadCount} + 1` })\n .where(eq(repairGuides.id, id));\n }\n\n\n\n async toggleBookmark(id: string): Promise<RepairGuide | undefined> {\n const [guide] = await db\n .update(repairGuides)\n .set({ isBookmarked: sql`NOT ${repairGuides.isBookmarked}` })\n .where(eq(repairGuides.id, id))\n .returning();\n return guide;\n }\n\n async searchRepairGuides(query: string): Promise<RepairGuide[]> {\n const searchTerm = `%${query.toLowerCase()}%`;\n return await db\n .select()\n .from(repairGuides)\n .where(\n or(\n like(repairGuides.title, searchTerm),\n like(repairGuides.description, searchTerm),\n like(repairGuides.category, searchTerm)\n )\n );\n }\n\n // Troubleshooting Flows operations\n async getTroubleshootingFlows(): Promise<TroubleshootingFlow[]> {\n return await db.select().from(troubleshootingFlows);\n }\n\n async getTroubleshootingFlow(id: string): Promise<TroubleshootingFlow | undefined> {\n const [flow] = await db.select().from(troubleshootingFlows).where(eq(troubleshootingFlows.id, id));\n return flow;\n }\n\n async createTroubleshootingFlow(flow: InsertTroubleshootingFlow): Promise<TroubleshootingFlow> {\n const [newFlow] = await db.insert(troubleshootingFlows).values(flow).returning();\n return newFlow;\n }\n\n // Device Components operations\n async getDeviceComponents(deviceType?: string): Promise<DeviceComponent[]> {\n if (deviceType) {\n return await db.select().from(deviceComponents).where(eq(deviceComponents.deviceType, deviceType));\n }\n return await db.select().from(deviceComponents);\n }\n\n async getDeviceComponent(id: string): Promise<DeviceComponent | undefined> {\n const [component] = await db.select().from(deviceComponents).where(eq(deviceComponents.id, id));\n return component;\n }\n\n async createDeviceComponent(component: InsertDeviceComponent): Promise<DeviceComponent> {\n const [newComponent] = await db.insert(deviceComponents).values(component).returning();\n return newComponent;\n }\n\n // Device Brands and Models\n async getDeviceBrands(deviceType?: string): Promise<DeviceBrand[]> {\n const query = db.select().from(deviceBrands);\n if (deviceType) {\n return await query.where(eq(deviceBrands.deviceType, deviceType)).orderBy(sql`${deviceBrands.popularity} DESC`);\n }\n return await query.orderBy(sql`${deviceBrands.popularity} DESC`);\n }\n\n async getDeviceModels(brandId?: string, deviceType?: string): Promise<DeviceModel[]> {\n let query = db.select().from(deviceModels);\n \n if (brandId) {\n query = query.where(eq(deviceModels.brandId, brandId));\n }\n \n return await query.orderBy(sql`${deviceModels.year} DESC, ${deviceModels.name} ASC`);\n }\n\n async getDeviceModel(id: string): Promise<DeviceModel | undefined> {\n const [model] = await db.select().from(deviceModels).where(eq(deviceModels.id, id));\n return model;\n }\n\n async getModelsByBrand(brandId: string): Promise<DeviceModel[]> {\n return await db.select()\n .from(deviceModels)\n .where(eq(deviceModels.brandId, brandId))\n .orderBy(sql`${deviceModels.year} DESC, ${deviceModels.name} ASC`);\n }\n\n async searchDeviceModels(query: string, deviceType?: string): Promise<DeviceModel[]> {\n return await db.select()\n .from(deviceModels)\n .where(\n or(\n like(deviceModels.name, `%${query}%`),\n like(deviceModels.series, `%${query}%`)\n )\n )\n .orderBy(sql`${deviceModels.guideCount} DESC`);\n }\n\n // Guide Compatibility\n async getCompatibleGuides(modelId: string): Promise<RepairGuide[]> {\n return await db.select()\n .from(repairGuides)\n .innerJoin(guideCompatibility, eq(repairGuides.id, guideCompatibility.guideId))\n .where(\n and(\n eq(guideCompatibility.modelId, modelId),\n eq(guideCompatibility.compatibility, 'compatible')\n )\n )\n .orderBy(sql`${repairGuides.viewCount} DESC`);\n }\n\n async getModelCompatibility(guideId: string): Promise<DeviceModel[]> {\n return await db.select()\n .from(deviceModels)\n .innerJoin(guideCompatibility, eq(deviceModels.id, guideCompatibility.modelId))\n .where(eq(guideCompatibility.guideId, guideId))\n .orderBy(sql`${deviceModels.name} ASC`);\n }\n\n async createUserGuide(guide: InsertRepairGuide): Promise<RepairGuide> {\n const [createdGuide] = await db\n .insert(repairGuides)\n .values(guide)\n .returning();\n return createdGuide;\n }\n}\n\nexport const storage = new DatabaseStorage();","size_bytes":10279},"server/vite.ts":{"content":"import express, { type Express } from \"express\";\nimport fs from \"fs\";\nimport path from \"path\";\nimport { createServer as createViteServer, createLogger } from \"vite\";\nimport { type Server } from \"http\";\nimport viteConfig from \"../vite.config\";\nimport { nanoid } from \"nanoid\";\n\nconst viteLogger = createLogger();\n\nexport function log(message: string, source = \"express\") {\n const formattedTime = new Date().toLocaleTimeString(\"en-US\", {\n hour: \"numeric\",\n minute: \"2-digit\",\n second: \"2-digit\",\n hour12: true,\n });\n\n console.log(`${formattedTime} [${source}] ${message}`);\n}\n\nexport async function setupVite(app: Express, server: Server) {\n const serverOptions = {\n middlewareMode: true,\n hmr: { server },\n allowedHosts: true as const,\n };\n\n const vite = await createViteServer({\n ...viteConfig,\n configFile: false,\n customLogger: {\n ...viteLogger,\n error: (msg, options) => {\n viteLogger.error(msg, options);\n process.exit(1);\n },\n },\n server: serverOptions,\n appType: \"custom\",\n });\n\n app.use(vite.middlewares);\n app.use(\"*\", async (req, res, next) => {\n const url = req.originalUrl;\n\n try {\n const clientTemplate = path.resolve(\n import.meta.dirname,\n \"..\",\n \"client\",\n \"index.html\",\n );\n\n // always reload the index.html file from disk incase it changes\n let template = await fs.promises.readFile(clientTemplate, \"utf-8\");\n template = template.replace(\n `src=\"/src/main.tsx\"`,\n `src=\"/src/main.tsx?v=${nanoid()}\"`,\n );\n const page = await vite.transformIndexHtml(url, template);\n res.status(200).set({ \"Content-Type\": \"text/html\" }).end(page);\n } catch (e) {\n vite.ssrFixStacktrace(e as Error);\n next(e);\n }\n });\n}\n\nexport function serveStatic(app: Express) {\n const distPath = path.resolve(import.meta.dirname, \"public\");\n\n if (!fs.existsSync(distPath)) {\n throw new Error(\n `Could not find the build directory: ${distPath}, make sure to build the client first`,\n );\n }\n\n app.use(express.static(distPath));\n\n // fall through to index.html if the file doesn't exist\n app.use(\"*\", (_req, res) => {\n res.sendFile(path.resolve(distPath, \"index.html\"));\n });\n}\n","size_bytes":2263},"shared/schema.ts":{"content":"import { sql } from \"drizzle-orm\";\nimport { pgTable, text, varchar, integer, boolean, json, timestamp, jsonb, index } from \"drizzle-orm/pg-core\";\nimport { createInsertSchema } from \"drizzle-zod\";\nimport { z } from \"zod\";\n\nexport const repairGuides = pgTable(\"repair_guides\", {\n id: varchar(\"id\").primaryKey().default(sql`gen_random_uuid()`),\n title: text(\"title\").notNull(),\n description: text(\"description\").notNull(),\n deviceType: text(\"device_type\").notNull(), // \"laptop\" | \"desktop\"\n category: text(\"category\").notNull(), // \"hardware\" | \"software\" | \"cleaning\" | \"upgrades\"\n difficulty: text(\"difficulty\").notNull(), // \"easy\" | \"medium\" | \"hard\"\n estimatedTime: text(\"estimated_time\").notNull(), // \"30-45 min\"\n toolsRequired: json(\"tools_required\").$type<string[]>().notNull(),\n safetyWarnings: json(\"safety_warnings\").$type<string[]>().notNull(),\n steps: json(\"steps\").$type<RepairStep[]>().notNull(),\n alternativeSolutions: text(\"alternative_solutions\"),\n imageUrl: text(\"image_url\").notNull(),\n viewCount: integer(\"view_count\").default(0),\n downloadCount: integer(\"download_count\").default(0),\n isBookmarked: boolean(\"is_bookmarked\").default(false),\n});\n\nexport const troubleshootingFlows = pgTable(\"troubleshooting_flows\", {\n id: varchar(\"id\").primaryKey().default(sql`gen_random_uuid()`),\n type: text(\"type\").notNull(), // \"power\" | \"performance\" | \"display\"\n title: text(\"title\").notNull(),\n description: text(\"description\").notNull(),\n steps: json(\"steps\").$type<TroubleshootingStep[]>().notNull(),\n});\n\nexport const deviceComponents = pgTable(\"device_components\", {\n id: varchar(\"id\").primaryKey().default(sql`gen_random_uuid()`),\n name: text(\"name\").notNull(),\n description: text(\"description\").notNull(),\n deviceType: text(\"device_type\").notNull(),\n category: text(\"category\").notNull(),\n safetyNotes: json(\"safety_notes\").$type<string[]>().notNull(),\n});\n\n// Type definitions for complex JSON fields\nexport type RepairStep = {\n stepNumber: number;\n title: string;\n description: string;\n imageUrl?: string;\n notes?: string[];\n warnings?: string[];\n};\n\nexport type TroubleshootingStep = {\n id: string;\n question: string;\n answers: {\n text: string;\n nextStepId?: string;\n solutionId?: string;\n }[];\n};\n\n// Insert schemas\nexport const insertRepairGuideSchema = createInsertSchema(repairGuides).omit({\n id: true,\n viewCount: true,\n isBookmarked: true,\n});\n\nexport const insertTroubleshootingFlowSchema = createInsertSchema(troubleshootingFlows).omit({\n id: true,\n});\n\nexport const insertDeviceComponentSchema = createInsertSchema(deviceComponents).omit({\n id: true,\n});\n\n// Types\nexport type RepairGuide = typeof repairGuides.$inferSelect;\nexport type InsertRepairGuide = z.infer<typeof insertRepairGuideSchema>;\n\nexport type TroubleshootingFlow = typeof troubleshootingFlows.$inferSelect;\nexport type InsertTroubleshootingFlow = z.infer<typeof insertTroubleshootingFlowSchema>;\n\nexport type DeviceComponent = typeof deviceComponents.$inferSelect;\nexport type InsertDeviceComponent = z.infer<typeof insertDeviceComponentSchema>;\n\n// Session storage table for Replit Auth\nexport const sessions = pgTable(\n \"sessions\",\n {\n sid: varchar(\"sid\").primaryKey(),\n sess: jsonb(\"sess\").notNull(),\n expire: timestamp(\"expire\").notNull(),\n },\n (table) => [index(\"IDX_session_expire\").on(table.expire)],\n);\n\n// User storage table for Replit Auth\nexport const users = pgTable(\"users\", {\n id: varchar(\"id\").primaryKey().default(sql`gen_random_uuid()`),\n email: varchar(\"email\").unique(),\n firstName: varchar(\"first_name\"),\n lastName: varchar(\"last_name\"),\n profileImageUrl: varchar(\"profile_image_url\"),\n role: varchar(\"role\").default(\"user\"), // \"user\" | \"admin\"\n createdAt: timestamp(\"created_at\").defaultNow(),\n updatedAt: timestamp(\"updated_at\").defaultNow(),\n});\n\n// Favorites table to store user's favorite guides and videos\nexport const favorites = pgTable(\"favorites\", {\n id: varchar(\"id\").primaryKey().default(sql`gen_random_uuid()`),\n userId: varchar(\"user_id\").notNull().references(() => users.id, { onDelete: \"cascade\" }),\n itemType: text(\"item_type\").notNull(), // \"guide\" | \"video\" | \"troubleshooting\"\n itemId: varchar(\"item_id\").notNull(), // ID of the favorited item\n itemTitle: text(\"item_title\").notNull(), // Cache the title for quick display\n itemImageUrl: text(\"item_image_url\"), // Cache image URL\n createdAt: timestamp(\"created_at\").defaultNow(),\n});\n\n// Device brands table\nexport const deviceBrands = pgTable(\"device_brands\", {\n id: varchar(\"id\").primaryKey().default(sql`gen_random_uuid()`),\n name: varchar(\"name\").notNull(),\n deviceType: text(\"device_type\").notNull(), // 'laptop' or 'desktop'\n logoUrl: varchar(\"logo_url\"),\n supportLevel: varchar(\"support_level\").notNull().default(\"community\"), // 'official', 'community', 'basic'\n popularity: integer(\"popularity\").default(0), // For sorting brands by popularity\n createdAt: timestamp(\"created_at\").defaultNow(),\n});\n\n// Device models table\nexport const deviceModels = pgTable(\"device_models\", {\n id: varchar(\"id\").primaryKey().default(sql`gen_random_uuid()`),\n brandId: varchar(\"brand_id\").notNull().references(() => deviceBrands.id),\n name: varchar(\"name\").notNull(),\n series: varchar(\"series\"), // e.g., \"ThinkPad\", \"Inspiron\", \"Pavilion\"\n year: integer(\"year\"),\n specifications: jsonb(\"specifications\"), // CPU, RAM, storage, etc.\n commonIssues: jsonb(\"common_issues\").default(sql`'[]'::jsonb`), // Array of common problems\n guideCount: integer(\"guide_count\").default(0), // Number of compatible guides\n createdAt: timestamp(\"created_at\").defaultNow(),\n});\n\n// Guide compatibility table (many-to-many between guides and models)\nexport const guideCompatibility = pgTable(\"guide_compatibility\", {\n id: varchar(\"id\").primaryKey().default(sql`gen_random_uuid()`),\n guideId: varchar(\"guide_id\").notNull().references(() => repairGuides.id),\n modelId: varchar(\"model_id\").notNull().references(() => deviceModels.id),\n compatibility: varchar(\"compatibility\").notNull().default(\"compatible\"), // 'compatible', 'partial', 'not_compatible'\n notes: text(\"notes\"), // Specific notes about compatibility\n createdAt: timestamp(\"created_at\").defaultNow(),\n});\n\n// Insert schemas for new tables\nexport const insertUserSchema = createInsertSchema(users).omit({\n id: true,\n createdAt: true,\n updatedAt: true,\n});\n\nexport const insertFavoriteSchema = createInsertSchema(favorites).omit({\n id: true,\n createdAt: true,\n});\n\nexport const insertDeviceBrandSchema = createInsertSchema(deviceBrands).omit({\n id: true,\n createdAt: true,\n});\n\nexport const insertDeviceModelSchema = createInsertSchema(deviceModels).omit({\n id: true,\n createdAt: true,\n});\n\nexport const insertGuideCompatibilitySchema = createInsertSchema(guideCompatibility).omit({\n id: true,\n createdAt: true,\n});\n\n// Types for new tables\nexport type User = typeof users.$inferSelect;\nexport type UpsertUser = typeof users.$inferInsert;\nexport type InsertUser = z.infer<typeof insertUserSchema>;\n\nexport type Favorite = typeof favorites.$inferSelect;\nexport type InsertFavorite = z.infer<typeof insertFavoriteSchema>;\n\nexport type DeviceBrand = typeof deviceBrands.$inferSelect;\nexport type InsertDeviceBrand = z.infer<typeof insertDeviceBrandSchema>;\n\nexport type DeviceModel = typeof deviceModels.$inferSelect;\nexport type InsertDeviceModel = z.infer<typeof insertDeviceModelSchema>;\n\nexport type GuideCompatibility = typeof guideCompatibility.$inferSelect;\nexport type InsertGuideCompatibility = z.infer<typeof insertGuideCompatibilitySchema>;\n","size_bytes":7580},"client/public/sw.js":{"content":"const CACHE_NAME = 'jcr-guide-pro-v1.0.0';\nconst urlsToCache = [\n '/',\n '/static/js/bundle.js',\n '/static/css/main.css',\n '/manifest.json',\n '/icon-192.png',\n '/icon-512.png'\n];\n\n// Install Service Worker\nself.addEventListener('install', (event) => {\n event.waitUntil(\n caches.open(CACHE_NAME)\n .then((cache) => {\n console.log('Opened cache');\n return cache.addAll(urlsToCache);\n })\n );\n});\n\n// Fetch\nself.addEventListener('fetch', (event) => {\n event.respondWith(\n caches.match(event.request)\n .then((response) => {\n // Return cached version or fetch from network\n if (response) {\n return response;\n }\n return fetch(event.request);\n }\n )\n );\n});\n\n// Activate\nself.addEventListener('activate', (event) => {\n event.waitUntil(\n caches.keys().then((cacheNames) => {\n return Promise.all(\n cacheNames.map((cacheName) => {\n if (cacheName !== CACHE_NAME) {\n return caches.delete(cacheName);\n }\n })\n );\n })\n );\n});\n\n// Background sync for offline functionality\nself.addEventListener('sync', (event) => {\n if (event.tag === 'background-sync') {\n event.waitUntil(doBackgroundSync());\n }\n});\n\nfunction doBackgroundSync() {\n // Handle background sync tasks\n console.log('Background sync triggered');\n}\n\n// Push notifications (if needed in future)\nself.addEventListener('push', (event) => {\n const options = {\n body: event.data ? event.data.text() : 'New update available',\n icon: '/icon-192.png',\n badge: '/icon-192.png'\n };\n\n event.waitUntil(\n self.registration.showNotification('JCR Guide Pro', options)\n );\n});","size_bytes":1675},"client/src/App.tsx":{"content":"import { Switch, Route } from \"wouter\";\nimport { queryClient } from \"./lib/queryClient\";\nimport { QueryClientProvider } from \"@tanstack/react-query\";\nimport { Toaster } from \"@/components/ui/toaster\";\nimport { InstallBanner } from \"@/components/ui/install-banner\";\nimport { TooltipProvider } from \"@/components/ui/tooltip\";\nimport { ThemeProvider } from \"@/contexts/theme-context\";\nimport { useAuth } from \"@/hooks/useAuth\";\nimport { lazy, Suspense } from \"react\";\n\n// Core pages loaded immediately for fast startup\nimport Home from \"@/pages/home\";\nimport Landing from \"@/pages/landing\";\n\n// Lazy load secondary pages to reduce initial bundle size\nconst Favorites = lazy(() => import(\"@/pages/favorites\"));\nconst RepairGuide = lazy(() => import(\"@/pages/repair-guide\"));\nconst Troubleshooting = lazy(() => import(\"@/pages/troubleshooting\"));\nconst SoftwareTools = lazy(() => import(\"@/pages/software-tools\"));\nconst DeviceFinder = lazy(() => import(\"@/pages/device-finder\"));\nconst CreateGuide = lazy(() => import(\"@/pages/create-guide\"));\nconst OfflineSettings = lazy(() => import(\"@/pages/offline-settings\"));\nconst Downloads = lazy(() => import(\"@/pages/downloads\"));\nconst AdminDashboard = lazy(() => import(\"@/pages/admin-dashboard\"));\nconst NotFound = lazy(() => import(\"@/pages/not-found\"));\n\nimport { PageLoadingSpinner } from \"@/components/ui/loading-spinner\";\n\nfunction Router() {\n const { isAuthenticated, isLoading } = useAuth();\n\n return (\n <Switch>\n {isLoading || !isAuthenticated ? (\n <Route path=\"/\" component={Landing} />\n ) : (\n <Suspense fallback={<PageLoadingSpinner />}>\n <Route path=\"/\" component={Home} />\n <Route path=\"/favorites\" component={Favorites} />\n <Route path=\"/guide/:id\" component={RepairGuide} />\n <Route path=\"/troubleshooting/:type?\" component={Troubleshooting} />\n <Route path=\"/software-tools\" component={SoftwareTools} />\n <Route path=\"/device-finder\" component={DeviceFinder} />\n <Route path=\"/create-guide\" component={CreateGuide} />\n <Route path=\"/offline-settings\" component={OfflineSettings} />\n <Route path=\"/downloads\" component={Downloads} />\n <Route path=\"/admin\" component={AdminDashboard} />\n </Suspense>\n )}\n <Suspense fallback={<PageLoadingSpinner />}>\n <Route component={NotFound} />\n </Suspense>\n </Switch>\n );\n}\n\nfunction App() {\n return (\n <ThemeProvider defaultTheme=\"light\" storageKey=\"jcr-guide-theme\">\n <QueryClientProvider client={queryClient}>\n <TooltipProvider>\n <Toaster />\n <InstallBanner />\n <Router />\n </TooltipProvider>\n </QueryClientProvider>\n </ThemeProvider>\n );\n}\n\nexport default App;\n","size_bytes":2771},"client/src/index.css":{"content":"@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&family=Roboto+Mono:wght@400;500&display=swap');\n@import url('https://fonts.googleapis.com/icon?family=Material+Icons');\n\n@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n:root {\n --background: hsl(210 13% 97%);\n --foreground: hsl(210 25% 7.8431%);\n --card: hsl(0 0% 100%);\n --card-foreground: hsl(210 25% 7.8431%);\n --popover: hsl(0 0% 100%);\n --popover-foreground: hsl(210 25% 7.8431%);\n --primary: hsl(207.4 90.2% 54.1%);\n --primary-foreground: hsl(0 0% 100%);\n --primary-700: hsl(210 79% 46%);\n --primary-900: hsl(215 84% 32%);\n --secondary: hsl(210 25% 7.8431%);\n --secondary-foreground: hsl(0 0% 100%);\n --muted: hsl(210 40% 96%);\n --muted-foreground: hsl(215.4 16.3% 46.9%);\n --accent: hsl(210 40% 96%);\n --accent-foreground: hsl(222.2 84% 4.9%);\n --destructive: hsl(0 84.2% 60.2%);\n --destructive-foreground: hsl(210 40% 98%);\n --border: hsl(214.3 31.8% 91.4%);\n --input: hsl(214.3 31.8% 91.4%);\n --ring: hsl(207.4 90.2% 54.1%);\n --success: hsl(122 39% 49%);\n --success-600: hsl(122 47% 35%);\n --warning: hsl(38 92% 50%);\n --warning-700: hsl(32 95% 44%);\n --danger: hsl(4 90% 58%);\n --danger-700: hsl(4 90% 45%);\n --purple-600: hsl(271 91% 65%);\n --cyan-600: hsl(197 71% 52%);\n --font-roboto: 'Roboto', sans-serif;\n --font-roboto-mono: 'Roboto Mono', monospace;\n --radius: 0.75rem;\n --shadow-material: 0 2px 4px rgba(0,0,0,0.1), 0 8px 16px rgba(0,0,0,0.1);\n --shadow-material-lg: 0 4px 8px rgba(0,0,0,0.12), 0 16px 32px rgba(0,0,0,0.12);\n}\n\n.dark {\n --background: hsl(222.2 84% 4.9%);\n --foreground: hsl(210 40% 98%);\n --card: hsl(222.2 84% 4.9%);\n --card-foreground: hsl(210 40% 98%);\n --popover: hsl(222.2 84% 4.9%);\n --popover-foreground: hsl(210 40% 98%);\n --primary: hsl(207.4 90.2% 54.1%);\n --primary-foreground: hsl(222.2 84% 4.9%);\n --primary-700: hsl(210 79% 46%);\n --primary-900: hsl(215 84% 32%);\n --secondary: hsl(217.2 32.6% 17.5%);\n --secondary-foreground: hsl(210 40% 98%);\n --muted: hsl(217.2 32.6% 17.5%);\n --muted-foreground: hsl(215 20.2% 65.1%);\n --accent: hsl(217.2 32.6% 17.5%);\n --accent-foreground: hsl(210 40% 98%);\n --destructive: hsl(0 62.8% 30.6%);\n --destructive-foreground: hsl(210 40% 98%);\n --border: hsl(217.2 32.6% 17.5%);\n --input: hsl(217.2 32.6% 17.5%);\n --ring: hsl(207.4 90.2% 54.1%);\n}\n\n.amoled {\n --background: hsl(0 0% 0%);\n --foreground: hsl(0 0% 98%);\n --card: hsl(0 0% 0%);\n --card-foreground: hsl(0 0% 98%);\n --popover: hsl(0 0% 0%);\n --popover-foreground: hsl(0 0% 98%);\n --primary: hsl(207.4 90.2% 54.1%);\n --primary-foreground: hsl(0 0% 0%);\n --primary-700: hsl(210 79% 46%);\n --primary-900: hsl(215 84% 32%);\n --secondary: hsl(0 0% 5%);\n --secondary-foreground: hsl(0 0% 98%);\n --muted: hsl(0 0% 5%);\n --muted-foreground: hsl(0 0% 65%);\n --accent: hsl(0 0% 5%);\n --accent-foreground: hsl(0 0% 98%);\n --destructive: hsl(0 62.8% 30.6%);\n --destructive-foreground: hsl(0 0% 98%);\n --border: hsl(0 0% 10%);\n --input: hsl(0 0% 10%);\n --ring: hsl(207.4 90.2% 54.1%);\n}\n\n@layer base {\n * {\n @apply border-border;\n }\n\n body {\n @apply bg-background text-foreground;\n font-family: var(--font-roboto);\n }\n\n .material-icons {\n font-family: 'Material Icons';\n font-weight: normal;\n font-style: normal;\n font-size: 24px;\n line-height: 1;\n letter-spacing: normal;\n text-transform: none;\n display: inline-block;\n white-space: nowrap;\n word-wrap: normal;\n direction: ltr;\n -webkit-font-feature-settings: 'liga';\n -webkit-font-smoothing: antialiased;\n }\n}\n\n@layer utilities {\n .shadow-material {\n box-shadow: var(--shadow-material);\n }\n \n .shadow-material-lg {\n box-shadow: var(--shadow-material-lg);\n }\n \n .text-primary-700 {\n color: var(--primary-700);\n }\n \n .text-primary-900 {\n color: var(--primary-900);\n }\n \n .bg-primary-700 {\n background-color: var(--primary-700);\n }\n \n .bg-primary-900 {\n background-color: var(--primary-900);\n }\n \n .hover\\:bg-primary-900:hover {\n background-color: var(--primary-900);\n }\n \n .text-success-600 {\n color: var(--success-600);\n }\n \n .bg-success-50 {\n background-color: hsl(122 39% 96%);\n }\n \n .text-warning-700 {\n color: var(--warning-700);\n }\n \n .bg-warning-50 {\n background-color: hsl(38 92% 95%);\n }\n \n .text-danger-700 {\n color: var(--danger-700);\n }\n \n .bg-danger-50 {\n background-color: hsl(4 90% 95%);\n }\n \n .text-purple-600 {\n color: var(--purple-600);\n }\n \n .bg-purple-50 {\n background-color: hsl(271 91% 95%);\n }\n \n .text-cyan-600 {\n color: var(--cyan-600);\n }\n}\n\n@media print {\n .no-print { \n display: none !important; \n }\n \n .shadow-material, \n .shadow-material-lg { \n box-shadow: none !important; \n }\n \n .bg-primary-700 { \n background-color: var(--primary-700) !important; \n color: white !important; \n }\n \n .text-primary-700 { \n color: var(--primary-700) !important; \n }\n}\n","size_bytes":5008},"client/src/main.tsx":{"content":"import { createRoot } from \"react-dom/client\";\nimport App from \"./App\";\nimport \"./index.css\";\n\n// Create root and render app immediately for faster startup\nconst root = createRoot(document.getElementById(\"root\")!);\nroot.render(<App />);\n\n// Load PWA functionality and performance optimizations after app renders (non-blocking)\nPromise.all([\n import(\"./pwa-utils\"),\n import(\"./utils/performance\")\n]).then(([pwaUtils, perfUtils]) => {\n // PWA setup\n pwaUtils.registerServiceWorker();\n pwaUtils.setupInstallPrompt();\n pwaUtils.setupOfflineDetection();\n \n // Performance optimizations\n perfUtils.preloadCriticalRoutes();\n perfUtils.setupImageOptimization();\n perfUtils.setupPreconnects();\n perfUtils.setupPerformanceMonitoring();\n}).catch(console.error);\n","size_bytes":763},"client/src/pwa-utils.ts":{"content":"// PWA utilities and service worker registration\n\nexport const registerServiceWorker = () => {\n if ('serviceWorker' in navigator) {\n window.addEventListener('load', () => {\n navigator.serviceWorker.register('/sw.js')\n .then((registration) => {\n console.log('SW registered: ', registration);\n })\n .catch((registrationError) => {\n console.log('SW registration failed: ', registrationError);\n });\n });\n }\n};\n\nexport const isStandalone = () => {\n return window.matchMedia('(display-mode: standalone)').matches || \n (window.navigator as any).standalone === true;\n};\n\nexport const canInstallPWA = () => {\n return 'serviceWorker' in navigator && 'PushManager' in window;\n};\n\n// Install prompt handling\nlet deferredPrompt: any;\n\nexport const setupInstallPrompt = () => {\n window.addEventListener('beforeinstallprompt', (e) => {\n // Prevent Chrome 67 and earlier from automatically showing the prompt\n e.preventDefault();\n // Stash the event so it can be triggered later\n deferredPrompt = e;\n \n // Show install button or banner\n showInstallPromotion();\n });\n\n window.addEventListener('appinstalled', () => {\n // Hide the install promotion\n hideInstallPromotion();\n console.log('PWA was installed');\n });\n};\n\nexport const showInstallPromotion = () => {\n // Show install banner or button\n const installBanner = document.getElementById('install-banner');\n if (installBanner) {\n installBanner.style.display = 'block';\n }\n};\n\nexport const hideInstallPromotion = () => {\n // Hide install banner or button\n const installBanner = document.getElementById('install-banner');\n if (installBanner) {\n installBanner.style.display = 'none';\n }\n};\n\nexport const installPWA = async () => {\n if (deferredPrompt) {\n // Show the install prompt\n deferredPrompt.prompt();\n // Wait for the user to respond to the prompt\n const { outcome } = await deferredPrompt.userChoice;\n console.log(`User response to the install prompt: ${outcome}`);\n // Clear the deferredPrompt variable\n deferredPrompt = null;\n }\n};\n\n// Check if app is running in standalone mode\nexport const isPWAInstalled = () => {\n return isStandalone() || document.referrer.includes('android-app://');\n};\n\n// Offline detection\nexport const setupOfflineDetection = () => {\n window.addEventListener('online', () => {\n console.log('App is online');\n // Show online indicator\n });\n\n window.addEventListener('offline', () => {\n console.log('App is offline');\n // Show offline indicator\n });\n};","size_bytes":2575},"server/middleware/adminAuth.ts":{"content":"import type { RequestHandler } from \"express\";\nimport { storage } from \"../storage\";\n\nexport const requireAdmin: RequestHandler = async (req, res, next) => {\n const user = req.user as any;\n\n if (!req.isAuthenticated() || !user.claims?.sub) {\n return res.status(401).json({ message: \"Unauthorized\" });\n }\n\n try {\n const dbUser = await storage.getUser(user.claims.sub);\n \n if (!dbUser || dbUser.role !== \"admin\") {\n return res.status(403).json({ message: \"Admin access required\" });\n }\n\n // Attach user info to request for convenience\n req.adminUser = dbUser;\n next();\n } catch (error) {\n console.error(\"Error checking admin status:\", error);\n res.status(500).json({ message: \"Failed to verify admin status\" });\n }\n};\n\n// Type declaration for TypeScript\ndeclare global {\n namespace Express {\n interface Request {\n adminUser?: {\n id: string;\n email: string | null;\n firstName: string | null;\n lastName: string | null;\n profileImageUrl: string | null;\n role: string | null;\n createdAt: Date | null;\n updatedAt: Date | null;\n };\n }\n }\n}","size_bytes":1146},"client/src/contexts/theme-context.tsx":{"content":"import { createContext, useContext, useEffect, useState } from \"react\";\n\ntype Theme = \"dark\" | \"light\" | \"amoled\" | \"system\";\n\ntype ThemeProviderProps = {\n children: React.ReactNode;\n defaultTheme?: Theme;\n storageKey?: string;\n};\n\ntype ThemeProviderState = {\n theme: Theme;\n setTheme: (theme: Theme) => void;\n};\n\nconst initialState: ThemeProviderState = {\n theme: \"system\",\n setTheme: () => null,\n};\n\nconst ThemeProviderContext = createContext<ThemeProviderState>(initialState);\n\nexport function ThemeProvider({\n children,\n defaultTheme = \"system\",\n storageKey = \"jcr-guide-theme\",\n ...props\n}: ThemeProviderProps) {\n const [theme, setTheme] = useState<Theme>(\n () => (localStorage.getItem(storageKey) as Theme) || defaultTheme\n );\n\n useEffect(() => {\n const root = window.document.documentElement;\n\n root.classList.remove(\"light\", \"dark\", \"amoled\");\n\n if (theme === \"system\") {\n const systemTheme = window.matchMedia(\"(prefers-color-scheme: dark)\")\n .matches\n ? \"dark\"\n : \"light\";\n\n root.classList.add(systemTheme);\n return;\n }\n\n root.classList.add(theme);\n }, [theme]);\n\n const value = {\n theme,\n setTheme: (theme: Theme) => {\n localStorage.setItem(storageKey, theme);\n setTheme(theme);\n },\n };\n\n return (\n <ThemeProviderContext.Provider {...props} value={value}>\n {children}\n </ThemeProviderContext.Provider>\n );\n}\n\nexport const useTheme = () => {\n const context = useContext(ThemeProviderContext);\n\n if (context === undefined)\n throw new Error(\"useTheme must be used within a ThemeProvider\");\n\n return context;\n};","size_bytes":1627},"client/src/hooks/use-mobile.tsx":{"content":"import * as React from \"react\"\n\nconst MOBILE_BREAKPOINT = 768\n\nexport function useIsMobile() {\n const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)\n\n React.useEffect(() => {\n const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)\n const onChange = () => {\n setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)\n }\n mql.addEventListener(\"change\", onChange)\n setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)\n return () => mql.removeEventListener(\"change\", onChange)\n }, [])\n\n return !!isMobile\n}\n","size_bytes":565},"client/src/hooks/use-toast.ts":{"content":"import * as React from \"react\"\n\nimport type {\n ToastActionElement,\n ToastProps,\n} from \"@/components/ui/toast\"\n\nconst TOAST_LIMIT = 1\nconst TOAST_REMOVE_DELAY = 1000000\n\ntype ToasterToast = ToastProps & {\n id: string\n title?: React.ReactNode\n description?: React.ReactNode\n action?: ToastActionElement\n}\n\nconst actionTypes = {\n ADD_TOAST: \"ADD_TOAST\",\n UPDATE_TOAST: \"UPDATE_TOAST\",\n DISMISS_TOAST: \"DISMISS_TOAST\",\n REMOVE_TOAST: \"REMOVE_TOAST\",\n} as const\n\nlet count = 0\n\nfunction genId() {\n count = (count + 1) % Number.MAX_SAFE_INTEGER\n return count.toString()\n}\n\ntype ActionType = typeof actionTypes\n\ntype Action =\n | {\n type: ActionType[\"ADD_TOAST\"]\n toast: ToasterToast\n }\n | {\n type: ActionType[\"UPDATE_TOAST\"]\n toast: Partial<ToasterToast>\n }\n | {\n type: ActionType[\"DISMISS_TOAST\"]\n toastId?: ToasterToast[\"id\"]\n }\n | {\n type: ActionType[\"REMOVE_TOAST\"]\n toastId?: ToasterToast[\"id\"]\n }\n\ninterface State {\n toasts: ToasterToast[]\n}\n\nconst toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()\n\nconst addToRemoveQueue = (toastId: string) => {\n if (toastTimeouts.has(toastId)) {\n return\n }\n\n const timeout = setTimeout(() => {\n toastTimeouts.delete(toastId)\n dispatch({\n type: \"REMOVE_TOAST\",\n toastId: toastId,\n })\n }, TOAST_REMOVE_DELAY)\n\n toastTimeouts.set(toastId, timeout)\n}\n\nexport const reducer = (state: State, action: Action): State => {\n switch (action.type) {\n case \"ADD_TOAST\":\n return {\n ...state,\n toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),\n }\n\n case \"UPDATE_TOAST\":\n return {\n ...state,\n toasts: state.toasts.map((t) =>\n t.id === action.toast.id ? { ...t, ...action.toast } : t\n ),\n }\n\n case \"DISMISS_TOAST\": {\n const { toastId } = action\n\n // ! Side effects ! - This could be extracted into a dismissToast() action,\n // but I'll keep it here for simplicity\n if (toastId) {\n addToRemoveQueue(toastId)\n } else {\n state.toasts.forEach((toast) => {\n addToRemoveQueue(toast.id)\n })\n }\n\n return {\n ...state,\n toasts: state.toasts.map((t) =>\n t.id === toastId || toastId === undefined\n ? {\n ...t,\n open: false,\n }\n : t\n ),\n }\n }\n case \"REMOVE_TOAST\":\n if (action.toastId === undefined) {\n return {\n ...state,\n toasts: [],\n }\n }\n return {\n ...state,\n toasts: state.toasts.filter((t) => t.id !== action.toastId),\n }\n }\n}\n\nconst listeners: Array<(state: State) => void> = []\n\nlet memoryState: State = { toasts: [] }\n\nfunction dispatch(action: Action) {\n memoryState = reducer(memoryState, action)\n listeners.forEach((listener) => {\n listener(memoryState)\n })\n}\n\ntype Toast = Omit<ToasterToast, \"id\">\n\nfunction toast({ ...props }: Toast) {\n const id = genId()\n\n const update = (props: ToasterToast) =>\n dispatch({\n type: \"UPDATE_TOAST\",\n toast: { ...props, id },\n })\n const dismiss = () => dispatch({ type: \"DISMISS_TOAST\", toastId: id })\n\n dispatch({\n type: \"ADD_TOAST\",\n toast: {\n ...props,\n id,\n open: true,\n onOpenChange: (open) => {\n if (!open) dismiss()\n },\n },\n })\n\n return {\n id: id,\n dismiss,\n update,\n }\n}\n\nfunction useToast() {\n const [state, setState] = React.useState<State>(memoryState)\n\n React.useEffect(() => {\n listeners.push(setState)\n return () => {\n const index = listeners.indexOf(setState)\n if (index > -1) {\n listeners.splice(index, 1)\n }\n }\n }, [state])\n\n return {\n ...state,\n toast,\n dismiss: (toastId?: string) => dispatch({ type: \"DISMISS_TOAST\", toastId }),\n }\n}\n\nexport { useToast, toast }\n","size_bytes":3895},"client/src/hooks/useAdmin.ts":{"content":"import { useQuery } from \"@tanstack/react-query\";\nimport { useAuth } from \"./useAuth\";\n\nexport function useAdmin() {\n const { user, isAuthenticated } = useAuth();\n \n const { data: adminData, isLoading } = useQuery({\n queryKey: [\"/api/admin/users\"],\n enabled: isAuthenticated && user?.role === \"admin\",\n retry: false,\n });\n\n return {\n isAdmin: user?.role === \"admin\",\n adminData,\n isLoading,\n };\n}\n\nexport function useAdminStats() {\n const { isAdmin } = useAdmin();\n \n const { data: stats, isLoading } = useQuery({\n queryKey: [\"/api/admin/stats\"],\n enabled: isAdmin,\n retry: false,\n });\n\n return {\n stats,\n isLoading,\n };\n}","size_bytes":667},"client/src/hooks/useAuth.ts":{"content":"import { useQuery } from \"@tanstack/react-query\";\n\nexport function useAuth() {\n const { data: user, isLoading } = useQuery({\n queryKey: [\"/api/auth/user\"],\n retry: false,\n });\n\n return {\n user,\n isLoading,\n isAuthenticated: !!user,\n };\n}","size_bytes":256},"client/src/hooks/useOfflineMode.ts":{"content":"import { useState, useEffect } from 'react';\nimport { useQuery, useQueryClient } from '@tanstack/react-query';\nimport { \n isOfflineSupported, \n isOnline, \n loadOfflineData, \n saveOfflineData, \n syncWhenOnline,\n getStorageUsage,\n type OfflineData \n} from '@/lib/offline-storage';\nimport type { RepairGuide, TroubleshootingFlow, DeviceComponent } from '@shared/schema';\n\nexport function useOfflineMode() {\n const [isOfflineModeEnabled, setIsOfflineModeEnabled] = useState(false);\n const [isCurrentlyOnline, setIsCurrentlyOnline] = useState(isOnline());\n const [lastSyncTime, setLastSyncTime] = useState<Date | null>(null);\n const queryClient = useQueryClient();\n\n // Monitor online/offline status\n useEffect(() => {\n const handleOnline = () => {\n setIsCurrentlyOnline(true);\n if (isOfflineModeEnabled) {\n syncWhenOnline();\n }\n };\n\n const handleOffline = () => {\n setIsCurrentlyOnline(false);\n };\n\n window.addEventListener('online', handleOnline);\n window.addEventListener('offline', handleOffline);\n\n return () => {\n window.removeEventListener('online', handleOnline);\n window.removeEventListener('offline', handleOffline);\n };\n }, [isOfflineModeEnabled]);\n\n // Load offline data on mount\n useEffect(() => {\n if (isOfflineSupported()) {\n const offlineData = loadOfflineData();\n if (offlineData.lastSync) {\n setLastSyncTime(new Date(offlineData.lastSync));\n }\n }\n }, []);\n\n // Enable offline mode and cache current data\n const enableOfflineMode = async (): Promise<boolean> => {\n if (!isOfflineSupported()) return false;\n\n try {\n // Get current data from React Query cache or fetch fresh\n const guides = queryClient.getQueryData<RepairGuide[]>(['/api/repair-guides']) || [];\n const flows = queryClient.getQueryData<TroubleshootingFlow[]>(['/api/troubleshooting-flows']) || [];\n const components = queryClient.getQueryData<DeviceComponent[]>(['/api/device-components']) || [];\n\n // If cache is empty, fetch fresh data\n let freshGuides = guides;\n let freshFlows = flows;\n let freshComponents = components;\n\n if (isCurrentlyOnline && (guides.length === 0 || flows.length === 0 || components.length === 0)) {\n try {\n const [guidesRes, flowsRes, componentsRes] = await Promise.all([\n fetch('/api/repair-guides').then(res => res.json()),\n fetch('/api/troubleshooting-flows').then(res => res.json()),\n fetch('/api/device-components').then(res => res.json()),\n ]);\n \n freshGuides = guidesRes || guides;\n freshFlows = flowsRes || flows;\n freshComponents = componentsRes || components;\n } catch (error) {\n console.error('Failed to fetch fresh data for offline mode:', error);\n }\n }\n\n await saveOfflineData({\n guides: freshGuides,\n flows: freshFlows,\n components: freshComponents,\n });\n\n setIsOfflineModeEnabled(true);\n setLastSyncTime(new Date());\n return true;\n } catch (error) {\n console.error('Failed to enable offline mode:', error);\n return false;\n }\n };\n\n // Disable offline mode\n const disableOfflineMode = () => {\n setIsOfflineModeEnabled(false);\n };\n\n // Get offline data\n const getOfflineData = (): OfflineData => {\n return loadOfflineData();\n };\n\n // Force sync data\n const forcSync = async (): Promise<boolean> => {\n if (!isCurrentlyOnline) return false;\n\n try {\n await syncWhenOnline();\n setLastSyncTime(new Date());\n \n // Invalidate React Query cache to refresh with new data\n queryClient.invalidateQueries({ queryKey: ['/api/repair-guides'] });\n queryClient.invalidateQueries({ queryKey: ['/api/troubleshooting-flows'] });\n queryClient.invalidateQueries({ queryKey: ['/api/device-components'] });\n \n return true;\n } catch (error) {\n console.error('Sync failed:', error);\n return false;\n }\n };\n\n // Get storage usage information\n const storageUsage = getStorageUsage();\n const storageInfo = {\n used: storageUsage.used,\n available: storageUsage.total - storageUsage.used,\n percentage: storageUsage.percentage\n };\n\n // Query for offline guides when offline mode is enabled and online\n const { data: offlineGuides = [] } = useQuery<RepairGuide[]>({\n queryKey: ['offline-guides'],\n queryFn: () => {\n const offlineData = getOfflineData();\n return offlineData.guides;\n },\n enabled: isOfflineModeEnabled && !isCurrentlyOnline,\n staleTime: Infinity,\n });\n\n const { data: offlineFlows = [] } = useQuery<TroubleshootingFlow[]>({\n queryKey: ['offline-flows'],\n queryFn: () => {\n const offlineData = getOfflineData();\n return offlineData.flows;\n },\n enabled: isOfflineModeEnabled && !isCurrentlyOnline,\n staleTime: Infinity,\n });\n\n const { data: offlineComponents = [] } = useQuery<DeviceComponent[]>({\n queryKey: ['offline-components'],\n queryFn: () => {\n const offlineData = getOfflineData();\n return offlineData.components;\n },\n enabled: isOfflineModeEnabled && !isCurrentlyOnline,\n staleTime: Infinity,\n });\n\n return {\n // Status\n isOfflineSupported: isOfflineSupported(),\n isOfflineModeEnabled,\n isCurrentlyOnline,\n lastSyncTime,\n storageInfo,\n\n // Actions\n enableOfflineMode,\n disableOfflineMode,\n forceSync: forcSync,\n getOfflineData,\n\n // Offline data\n offlineGuides,\n offlineFlows,\n offlineComponents,\n };\n}","size_bytes":5573},"client/src/hooks/useOptimizedQuery.ts":{"content":"import { useQuery, UseQueryOptions } from \"@tanstack/react-query\";\nimport { useCallback, useMemo } from \"react\";\n\n// Optimized query hook with intelligent caching and background updates\nexport function useOptimizedQuery<T>(\n queryKey: string[],\n queryFn: () => Promise<T>,\n options?: Partial<UseQueryOptions<T>>\n) {\n // Memoize the query function to prevent unnecessary re-renders\n const memoizedQueryFn = useCallback(queryFn, [queryKey.join('/')]);\n\n // Optimized query options for better performance\n const optimizedOptions = useMemo(() => ({\n staleTime: 5 * 60 * 1000, // 5 minutes\n gcTime: 10 * 60 * 1000, // 10 minutes \n refetchOnWindowFocus: false,\n refetchOnReconnect: true,\n retry: (failureCount: number, error: any) => {\n // Don't retry on 4xx errors except 408 (timeout)\n if (error?.status >= 400 && error?.status < 500 && error?.status !== 408) {\n return false;\n }\n return failureCount < 2;\n },\n retryDelay: (attemptIndex: number) => Math.min(1000 * 2 ** attemptIndex, 30000),\n ...options,\n }), [options]);\n\n return useQuery({\n queryKey,\n queryFn: memoizedQueryFn,\n ...optimizedOptions,\n });\n}\n\n// Hook for data that updates frequently\nexport function useLiveQuery<T>(\n queryKey: string[],\n queryFn: () => Promise<T>,\n options?: Partial<UseQueryOptions<T>>\n) {\n return useOptimizedQuery(queryKey, queryFn, {\n staleTime: 30 * 1000, // 30 seconds for live data\n refetchInterval: 60 * 1000, // Auto-refetch every minute\n ...options,\n });\n}\n\n// Hook for static/rarely changing data\nexport function useStaticQuery<T>(\n queryKey: string[],\n queryFn: () => Promise<T>,\n options?: Partial<UseQueryOptions<T>>\n) {\n return useOptimizedQuery(queryKey, queryFn, {\n staleTime: 60 * 60 * 1000, // 1 hour for static data\n gcTime: 24 * 60 * 60 * 1000, // 24 hours cache\n ...options,\n });\n}","size_bytes":1883},"client/src/lib/authUtils.ts":{"content":"export function isUnauthorizedError(error: Error): boolean {\n return /^401: .*Unauthorized/.test(error.message);\n}","size_bytes":115},"client/src/lib/download-utils.ts":{"content":"// Download utilities for guides and videos\nimport type { RepairGuide, TroubleshootingFlow } from \"@shared/schema\";\nimport { saveDownloadedFile, type DownloadedFile } from \"./offline-storage\";\n\nexport interface DownloadOptions {\n includeImages?: boolean;\n includeVideos?: boolean;\n format?: 'pdf' | 'html' | 'json';\n}\n\n// Download a repair guide as a file\nexport const downloadGuide = async (guide: RepairGuide, options: DownloadOptions = {}) => {\n const { format = 'html', includeImages = true } = options;\n \n try {\n let content: string;\n let mimeType: string;\n let fileName: string;\n\n switch (format) {\n case 'html':\n content = generateHTMLGuide(guide, includeImages);\n mimeType = 'text/html';\n fileName = `${sanitizeFileName(guide.title)}.html`;\n break;\n case 'json':\n content = JSON.stringify(guide, null, 2);\n mimeType = 'application/json';\n fileName = `${sanitizeFileName(guide.title)}.json`;\n break;\n case 'pdf':\n // For PDF, we'll generate HTML and let the browser handle PDF conversion\n content = generatePrintableHTML(guide, includeImages);\n mimeType = 'text/html';\n fileName = `${sanitizeFileName(guide.title)}_printable.html`;\n break;\n default:\n throw new Error('Unsupported format');\n }\n\n const blob = new Blob([content], { type: mimeType });\n const url = URL.createObjectURL(blob);\n \n const link = document.createElement('a');\n link.href = url;\n link.download = fileName;\n link.style.display = 'none';\n \n document.body.appendChild(link);\n link.click();\n document.body.removeChild(link);\n \n URL.revokeObjectURL(url);\n \n // Track the download\n await trackGuideDownload(guide.id);\n const downloadRecord: DownloadedFile = {\n id: `guide_${guide.id}_${Date.now()}`,\n name: guide.title,\n type: 'guide',\n size: blob.size,\n downloadDate: new Date().toISOString(),\n lastAccessed: new Date().toISOString(),\n format: format,\n relatedGuideId: guide.id,\n };\n saveDownloadedFile(downloadRecord);\n \n return true;\n } catch (error) {\n console.error('Failed to download guide:', error);\n return false;\n }\n};\n\n// Download video from URL\nexport const downloadVideo = async (videoUrl: string, title: string): Promise<boolean> => {\n try {\n const response = await fetch(videoUrl, {\n mode: 'cors',\n });\n \n if (!response.ok) {\n throw new Error(`HTTP error! status: ${response.status}`);\n }\n \n const blob = await response.blob();\n const url = URL.createObjectURL(blob);\n \n const link = document.createElement('a');\n link.href = url;\n link.download = `${sanitizeFileName(title)}.mp4`;\n link.style.display = 'none';\n \n document.body.appendChild(link);\n link.click();\n document.body.removeChild(link);\n \n URL.revokeObjectURL(url);\n \n // Track the video download\n const downloadRecord: DownloadedFile = {\n id: `video_${Date.now()}`,\n name: title,\n type: 'video',\n size: blob.size,\n downloadDate: new Date().toISOString(),\n lastAccessed: new Date().toISOString(),\n format: 'mp4',\n };\n saveDownloadedFile(downloadRecord);\n \n return true;\n } catch (error) {\n console.error('Failed to download video:', error);\n return false;\n }\n};\n\n// Download multiple guides as a ZIP file\nexport const downloadGuidePackage = async (guides: RepairGuide[], packageName: string = 'repair_guides'): Promise<boolean> => {\n try {\n // For now, we'll create individual files. In production, you'd use a ZIP library\n const promises = guides.map(guide => \n downloadGuide(guide, { format: 'html', includeImages: true })\n );\n \n await Promise.all(promises);\n return true;\n } catch (error) {\n console.error('Failed to download guide package:', error);\n return false;\n }\n};\n\n// Generate HTML content for a guide\nconst generateHTMLGuide = (guide: RepairGuide, includeImages: boolean): string => {\n const steps = guide.steps as Array<{\n stepNumber: number;\n title: string;\n description: string;\n notes?: string[];\n warnings?: string[];\n }> || [];\n\n return `\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>${guide.title}</title>\n <style>\n body { \n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n line-height: 1.6; \n max-width: 800px; \n margin: 0 auto; \n padding: 20px;\n color: #333;\n }\n .header { \n border-bottom: 2px solid #e2e8f0; \n padding-bottom: 20px; \n margin-bottom: 30px; \n }\n .title { \n font-size: 2.5rem; \n font-weight: bold; \n margin-bottom: 10px;\n color: #1a202c;\n }\n .description { \n font-size: 1.1rem; \n color: #4a5568; \n margin-bottom: 15px;\n }\n .meta { \n display: flex; \n gap: 15px; \n font-size: 0.9rem;\n color: #718096;\n }\n .difficulty { \n padding: 4px 12px; \n border-radius: 6px; \n font-weight: 500;\n }\n .difficulty.easy { background: #c6f6d5; color: #22543d; }\n .difficulty.medium { background: #fef2c0; color: #744210; }\n .difficulty.hard { background: #fed7d7; color: #742a2a; }\n .section { \n margin: 30px 0; \n }\n .section-title { \n font-size: 1.5rem; \n font-weight: 600; \n margin-bottom: 15px;\n color: #2d3748;\n }\n .tools-list, .safety-list { \n list-style: none; \n padding: 0; \n }\n .tools-list li, .safety-list li { \n padding: 8px 12px; \n margin: 5px 0; \n border-left: 4px solid #4299e1;\n background: #ebf8ff;\n }\n .safety-list li { \n border-left-color: #f56565; \n background: #fed7d7;\n }\n .step { \n margin: 25px 0; \n padding: 20px; \n border: 1px solid #e2e8f0; \n border-radius: 8px;\n background: #f7fafc;\n }\n .step-number { \n font-size: 1.3rem; \n font-weight: bold; \n color: #3182ce;\n margin-bottom: 10px;\n }\n .step-title { \n font-size: 1.2rem; \n font-weight: 600; \n margin-bottom: 10px;\n color: #2d3748;\n }\n .step-description { \n margin-bottom: 15px; \n line-height: 1.8;\n }\n .notes, .warnings { \n margin-top: 10px; \n padding: 10px; \n border-radius: 6px;\n font-size: 0.9rem;\n }\n .notes { \n background: #ebf8ff; \n border-left: 4px solid #4299e1;\n }\n .warnings { \n background: #fed7d7; \n border-left: 4px solid #f56565;\n }\n .guide-image { \n max-width: 100%; \n height: auto; \n border-radius: 8px; \n margin: 20px 0;\n }\n @media print {\n body { font-size: 12pt; }\n .step { break-inside: avoid; }\n }\n </style>\n</head>\n<body>\n <div class=\"header\">\n <h1 class=\"title\">${guide.title}</h1>\n <p class=\"description\">${guide.description}</p>\n <div class=\"meta\">\n <span class=\"difficulty ${guide.difficulty}\">${guide.difficulty.toUpperCase()}</span>\n <span>📱 ${guide.deviceType}</span>\n <span>🔧 ${guide.category}</span>\n <span>⏱️ ${guide.estimatedTime}</span>\n </div>\n </div>\n\n ${includeImages && guide.imageUrl ? `\n <div class=\"section\">\n <img src=\"${guide.imageUrl}\" alt=\"${guide.title}\" class=\"guide-image\" />\n </div>\n ` : ''}\n\n <div class=\"section\">\n <h2 class=\"section-title\">🛠️ Tools Required</h2>\n <ul class=\"tools-list\">\n ${guide.toolsRequired.map(tool => `<li>${tool}</li>`).join('')}\n </ul>\n </div>\n\n ${guide.safetyWarnings && guide.safetyWarnings.length > 0 ? `\n <div class=\"section\">\n <h2 class=\"section-title\">⚠️ Safety Warnings</h2>\n <ul class=\"safety-list\">\n ${guide.safetyWarnings.map(warning => `<li>${warning}</li>`).join('')}\n </ul>\n </div>\n ` : ''}\n\n <div class=\"section\">\n <h2 class=\"section-title\">📋 Repair Steps</h2>\n ${steps.map(step => `\n <div class=\"step\">\n <div class=\"step-number\">Step ${step.stepNumber}</div>\n <div class=\"step-title\">${step.title}</div>\n <div class=\"step-description\">${step.description}</div>\n ${step.notes && step.notes.length > 0 ? `\n <div class=\"notes\">\n <strong>Notes:</strong> ${step.notes.join(', ')}\n </div>\n ` : ''}\n ${step.warnings && step.warnings.length > 0 ? `\n <div class=\"warnings\">\n <strong>⚠️ Warnings:</strong> ${step.warnings.join(', ')}\n </div>\n ` : ''}\n </div>\n `).join('')}\n </div>\n\n ${guide.alternativeSolutions ? `\n <div class=\"section\">\n <h2 class=\"section-title\">🔄 Alternative Solutions</h2>\n <p>${guide.alternativeSolutions}</p>\n </div>\n ` : ''}\n\n <div class=\"section\" style=\"margin-top: 50px; padding-top: 20px; border-top: 1px solid #e2e8f0; text-align: center; color: #718096; font-size: 0.9rem;\">\n <p>Downloaded from JCR Guide Pro • ${new Date().toLocaleDateString()}</p>\n </div>\n</body>\n</html>\n `.trim();\n};\n\n// Generate printable HTML (optimized for PDF)\nconst generatePrintableHTML = (guide: RepairGuide, includeImages: boolean): string => {\n const html = generateHTMLGuide(guide, includeImages);\n return html.replace(\n '<style>',\n '<style>@page { margin: 1in; } body { font-size: 11pt; } '\n );\n};\n\n// Sanitize filename for downloads\nconst sanitizeFileName = (fileName: string): string => {\n return fileName\n .replace(/[^a-z0-9]/gi, '_')\n .replace(/_+/g, '_')\n .replace(/^_|_$/g, '')\n .toLowerCase();\n};\n\n// Check if download is supported\nexport const isDownloadSupported = (): boolean => {\n return typeof document !== 'undefined' && 'createElement' in document;\n};\n\n// Get estimated download size\nexport const getEstimatedSize = (guide: RepairGuide, options: DownloadOptions = {}): string => {\n const { format = 'html', includeImages = true } = options;\n \n let estimatedBytes = 0;\n \n // Base content size\n estimatedBytes += JSON.stringify(guide).length * 2; // HTML markup overhead\n \n // Add estimated image sizes if included\n if (includeImages && guide.imageUrl) {\n estimatedBytes += 100000; // Estimate 100KB per image\n }\n \n // Format-specific overhead\n switch (format) {\n case 'html':\n estimatedBytes += 5000; // CSS and HTML structure\n break;\n case 'pdf':\n estimatedBytes *= 1.5; // PDF conversion overhead\n break;\n }\n \n return formatFileSize(estimatedBytes);\n};\n\n// Format file size for display\nconst formatFileSize = (bytes: number): string => {\n if (bytes === 0) return '0 B';\n const k = 1024;\n const sizes = ['B', 'KB', 'MB', 'GB'];\n const i = Math.floor(Math.log(bytes) / Math.log(k));\n return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];\n};\n\n// Track guide download\nexport const trackGuideDownload = async (guideId: string): Promise<void> => {\n try {\n await fetch(`/api/repair-guides/${guideId}/download`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n });\n } catch (error) {\n console.error('Failed to track download:', error);\n }\n};","size_bytes":11969},"client/src/lib/offline-storage.ts":{"content":"// Offline storage utilities for repair guides\nimport type { RepairGuide, TroubleshootingFlow, DeviceComponent } from \"@shared/schema\";\n\nconst STORAGE_KEYS = {\n GUIDES: 'jcr_offline_guides',\n FLOWS: 'jcr_offline_flows', \n COMPONENTS: 'jcr_offline_components',\n FAVORITES: 'jcr_offline_favorites',\n LAST_SYNC: 'jcr_last_sync',\n USER_GUIDES: 'jcr_user_guides',\n DOWNLOADS: 'jcr_downloaded_files',\n VIDEOS: 'jcr_offline_videos',\n IMAGES: 'jcr_cached_images',\n\n} as const;\n\nexport interface OfflineData {\n guides: RepairGuide[];\n flows: TroubleshootingFlow[];\n components: DeviceComponent[];\n favorites: string[];\n userGuides: RepairGuide[];\n lastSync: string;\n}\n\nexport interface StorageUsage {\n used: number;\n total: number;\n percentage: number;\n breakdown: {\n guides: number;\n flows: number;\n components: number;\n favorites: number;\n userGuides: number;\n downloads: number;\n videos: number;\n images: number;\n\n };\n}\n\nexport interface DownloadedFile {\n id: string;\n name: string;\n type: 'guide' | 'video' | 'image' | 'document';\n size: number;\n downloadDate: string;\n lastAccessed: string;\n format: string;\n relatedGuideId?: string;\n}\n\n\n\n// Check if browser supports offline storage\nexport const isOfflineSupported = (): boolean => {\n try {\n return typeof localStorage !== 'undefined' && \n typeof navigator !== 'undefined' &&\n 'serviceWorker' in navigator;\n } catch {\n return false;\n }\n};\n\n// Save data for offline use\nexport const saveOfflineData = async (data: Partial<OfflineData>): Promise<void> => {\n if (!isOfflineSupported()) return;\n\n try {\n if (data.guides) {\n localStorage.setItem(STORAGE_KEYS.GUIDES, JSON.stringify(data.guides));\n }\n if (data.flows) {\n localStorage.setItem(STORAGE_KEYS.FLOWS, JSON.stringify(data.flows));\n }\n if (data.components) {\n localStorage.setItem(STORAGE_KEYS.COMPONENTS, JSON.stringify(data.components));\n }\n if (data.favorites) {\n localStorage.setItem(STORAGE_KEYS.FAVORITES, JSON.stringify(data.favorites));\n }\n if (data.userGuides) {\n localStorage.setItem(STORAGE_KEYS.USER_GUIDES, JSON.stringify(data.userGuides));\n }\n \n localStorage.setItem(STORAGE_KEYS.LAST_SYNC, new Date().toISOString());\n } catch (error) {\n console.error('Failed to save offline data:', error);\n }\n};\n\n// Load offline data\nexport const loadOfflineData = (): OfflineData => {\n const defaultData: OfflineData = {\n guides: [],\n flows: [],\n components: [],\n favorites: [],\n userGuides: [],\n lastSync: '',\n };\n\n if (!isOfflineSupported()) return defaultData;\n\n try {\n return {\n guides: JSON.parse(localStorage.getItem(STORAGE_KEYS.GUIDES) || '[]'),\n flows: JSON.parse(localStorage.getItem(STORAGE_KEYS.FLOWS) || '[]'),\n components: JSON.parse(localStorage.getItem(STORAGE_KEYS.COMPONENTS) || '[]'),\n favorites: JSON.parse(localStorage.getItem(STORAGE_KEYS.FAVORITES) || '[]'),\n userGuides: JSON.parse(localStorage.getItem(STORAGE_KEYS.USER_GUIDES) || '[]'),\n lastSync: localStorage.getItem(STORAGE_KEYS.LAST_SYNC) || '',\n };\n } catch (error) {\n console.error('Failed to load offline data:', error);\n return defaultData;\n }\n};\n\n// Save user-created guide locally\nexport const saveUserGuide = (guide: RepairGuide): void => {\n if (!isOfflineSupported()) return;\n\n try {\n const userGuides = loadOfflineData().userGuides;\n const updatedGuides = [...userGuides, { ...guide, id: `user_${Date.now()}` }];\n localStorage.setItem(STORAGE_KEYS.USER_GUIDES, JSON.stringify(updatedGuides));\n } catch (error) {\n console.error('Failed to save user guide:', error);\n }\n};\n\n// Get user-created guides\nexport const getUserGuides = (): RepairGuide[] => {\n if (!isOfflineSupported()) return [];\n return loadOfflineData().userGuides;\n};\n\n// Clear all offline data\nexport const clearOfflineData = (): void => {\n if (!isOfflineSupported()) return;\n\n try {\n Object.values(STORAGE_KEYS).forEach(key => {\n localStorage.removeItem(key);\n });\n } catch (error) {\n console.error('Failed to clear offline data:', error);\n }\n};\n\n// Get comprehensive storage usage info\nexport const getStorageUsage = (): StorageUsage => {\n if (!isOfflineSupported()) {\n return {\n used: 0,\n total: 0,\n percentage: 0,\n breakdown: {\n guides: 0,\n flows: 0,\n components: 0,\n favorites: 0,\n userGuides: 0,\n downloads: 0,\n videos: 0,\n images: 0,\n analytics: 0,\n }\n };\n }\n\n try {\n const breakdown = {\n guides: getItemSize(STORAGE_KEYS.GUIDES),\n flows: getItemSize(STORAGE_KEYS.FLOWS),\n components: getItemSize(STORAGE_KEYS.COMPONENTS),\n favorites: getItemSize(STORAGE_KEYS.FAVORITES),\n userGuides: getItemSize(STORAGE_KEYS.USER_GUIDES),\n downloads: getItemSize(STORAGE_KEYS.DOWNLOADS),\n videos: getItemSize(STORAGE_KEYS.VIDEOS),\n images: getItemSize(STORAGE_KEYS.IMAGES),\n\n };\n\n const used = Object.values(breakdown).reduce((sum, size) => sum + size, 0);\n const total = 50 * 1024 * 1024; // 50MB increased capacity\n const percentage = Math.min((used / total) * 100, 100);\n\n return { used, total, percentage, breakdown };\n } catch (error) {\n console.error('Failed to get storage usage:', error);\n return {\n used: 0,\n total: 50 * 1024 * 1024,\n percentage: 0,\n breakdown: {\n guides: 0,\n flows: 0,\n components: 0,\n favorites: 0,\n userGuides: 0,\n downloads: 0,\n videos: 0,\n images: 0,\n analytics: 0,\n }\n };\n }\n};\n\n// Helper function to get size of localStorage item\nconst getItemSize = (key: string): number => {\n try {\n const item = localStorage.getItem(key);\n return item ? new Blob([item]).size : 0;\n } catch {\n return 0;\n }\n};\n\n// Check if device is online\nexport const isOnline = (): boolean => {\n return typeof navigator !== 'undefined' ? navigator.onLine : true;\n};\n\n// Sync data when connection is restored\nexport const syncWhenOnline = async (): Promise<void> => {\n if (!isOnline() || !isOfflineSupported()) return;\n\n try {\n // Get pending user guides\n const userGuides = getUserGuides();\n const pendingGuides = userGuides.filter(guide => guide.id.startsWith('user_'));\n\n // Attempt to sync pending guides to server\n for (const guide of pendingGuides) {\n try {\n const response = await fetch('/api/repair-guides', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(guide),\n });\n\n if (response.ok) {\n // Remove from local storage after successful sync\n const remainingGuides = userGuides.filter(g => g.id !== guide.id);\n localStorage.setItem(STORAGE_KEYS.USER_GUIDES, JSON.stringify(remainingGuides));\n }\n } catch (error) {\n console.error('Failed to sync guide:', error);\n // Keep guide in local storage for next sync attempt\n }\n }\n\n // Refresh cached data\n const [guides, flows, components] = await Promise.all([\n fetch('/api/repair-guides').then(res => res.json()).catch(() => []),\n fetch('/api/troubleshooting-flows').then(res => res.json()).catch(() => []),\n fetch('/api/device-components').then(res => res.json()).catch(() => []),\n ]);\n\n await saveOfflineData({ guides, flows, components });\n } catch (error) {\n console.error('Sync failed:', error);\n }\n};\n\n// Format storage size for display\nexport const formatStorageSize = (bytes: number): string => {\n if (bytes === 0) return '0 B';\n const k = 1024;\n const sizes = ['B', 'KB', 'MB', 'GB'];\n const i = Math.floor(Math.log(bytes) / Math.log(k));\n return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];\n};\n\n// Save downloaded file record\nexport const saveDownloadedFile = (file: DownloadedFile): void => {\n if (!isOfflineSupported()) return;\n\n try {\n const downloads = getDownloadedFiles();\n const updatedDownloads = [...downloads, { ...file, lastAccessed: new Date().toISOString() }];\n localStorage.setItem(STORAGE_KEYS.DOWNLOADS, JSON.stringify(updatedDownloads));\n\n } catch (error) {\n console.error('Failed to save downloaded file record:', error);\n }\n};\n\n// Get all downloaded files\nexport const getDownloadedFiles = (): DownloadedFile[] => {\n if (!isOfflineSupported()) return [];\n\n try {\n const downloads = localStorage.getItem(STORAGE_KEYS.DOWNLOADS);\n return downloads ? JSON.parse(downloads) : [];\n } catch (error) {\n console.error('Failed to get downloaded files:', error);\n return [];\n }\n};\n\n// Update file access time\nexport const updateFileAccess = (fileId: string): void => {\n if (!isOfflineSupported()) return;\n\n try {\n const downloads = getDownloadedFiles();\n const updated = downloads.map(file => \n file.id === fileId \n ? { ...file, lastAccessed: new Date().toISOString() }\n : file\n );\n localStorage.setItem(STORAGE_KEYS.DOWNLOADS, JSON.stringify(updated));\n } catch (error) {\n console.error('Failed to update file access:', error);\n }\n};\n\n// Remove downloaded file record\nexport const removeDownloadedFile = (fileId: string): void => {\n if (!isOfflineSupported()) return;\n\n try {\n const downloads = getDownloadedFiles();\n const filtered = downloads.filter(file => file.id !== fileId);\n localStorage.setItem(STORAGE_KEYS.DOWNLOADS, JSON.stringify(filtered));\n } catch (error) {\n console.error('Failed to remove downloaded file:', error);\n }\n};\n\n\n\n\n\n\n\n// Clean up old downloads based on usage\nexport const cleanupOldDownloads = (maxAgeInDays: number = 30): void => {\n if (!isOfflineSupported()) return;\n\n try {\n const downloads = getDownloadedFiles();\n const cutoffDate = new Date();\n cutoffDate.setDate(cutoffDate.getDate() - maxAgeInDays);\n\n const activeDownloads = downloads.filter(file => {\n const lastAccessed = new Date(file.lastAccessed);\n return lastAccessed > cutoffDate;\n });\n\n localStorage.setItem(STORAGE_KEYS.DOWNLOADS, JSON.stringify(activeDownloads));\n } catch (error) {\n console.error('Failed to cleanup old downloads:', error);\n }\n};\n\n// Get storage recommendations\nexport const getStorageRecommendations = (): string[] => {\n const usage = getStorageUsage();\n const recommendations: string[] = [];\n\n if (usage.percentage > 90) {\n recommendations.push('Storage is almost full. Consider cleaning up old downloads.');\n }\n\n if (usage.percentage > 75) {\n recommendations.push('Storage usage is high. Remove unused content to free up space.');\n }\n\n if (usage.breakdown.videos > usage.breakdown.guides) {\n recommendations.push('Videos take up more space than guides. Consider downloading fewer videos.');\n }\n\n if (usage.breakdown.images > 5 * 1024 * 1024) { // 5MB\n recommendations.push('Cached images are using significant space. Clear image cache if needed.');\n }\n\n\n\n return recommendations;\n};\n\n// Export cache management for videos and images\nexport const cacheVideo = (videoId: string, videoData: Blob): Promise<boolean> => {\n return new Promise((resolve) => {\n if (!isOfflineSupported()) {\n resolve(false);\n return;\n }\n\n try {\n const reader = new FileReader();\n reader.onload = () => {\n try {\n const videos = JSON.parse(localStorage.getItem(STORAGE_KEYS.VIDEOS) || '{}');\n videos[videoId] = reader.result;\n localStorage.setItem(STORAGE_KEYS.VIDEOS, JSON.stringify(videos));\n resolve(true);\n } catch (error) {\n console.error('Failed to cache video:', error);\n resolve(false);\n }\n };\n reader.onerror = () => resolve(false);\n reader.readAsDataURL(videoData);\n } catch (error) {\n console.error('Failed to cache video:', error);\n resolve(false);\n }\n });\n};\n\nexport const getCachedVideo = (videoId: string): string | null => {\n if (!isOfflineSupported()) return null;\n\n try {\n const videos = JSON.parse(localStorage.getItem(STORAGE_KEYS.VIDEOS) || '{}');\n return videos[videoId] || null;\n } catch (error) {\n console.error('Failed to get cached video:', error);\n return null;\n }\n};\n\nexport const cacheImage = (imageUrl: string, imageData: Blob): Promise<boolean> => {\n return new Promise((resolve) => {\n if (!isOfflineSupported()) {\n resolve(false);\n return;\n }\n\n try {\n const reader = new FileReader();\n reader.onload = () => {\n try {\n const images = JSON.parse(localStorage.getItem(STORAGE_KEYS.IMAGES) || '{}');\n images[imageUrl] = reader.result;\n localStorage.setItem(STORAGE_KEYS.IMAGES, JSON.stringify(images));\n resolve(true);\n } catch (error) {\n console.error('Failed to cache image:', error);\n resolve(false);\n }\n };\n reader.onerror = () => resolve(false);\n reader.readAsDataURL(imageData);\n } catch (error) {\n console.error('Failed to cache image:', error);\n resolve(false);\n }\n });\n};\n\nexport const getCachedImage = (imageUrl: string): string | null => {\n if (!isOfflineSupported()) return null;\n\n try {\n const images = JSON.parse(localStorage.getItem(STORAGE_KEYS.IMAGES) || '{}');\n return images[imageUrl] || null;\n } catch (error) {\n console.error('Failed to get cached image:', error);\n return null;\n }\n};","size_bytes":13444},"client/src/lib/queryClient.ts":{"content":"import { QueryClient, QueryFunction } from \"@tanstack/react-query\";\n\nasync function throwIfResNotOk(res: Response) {\n if (!res.ok) {\n const text = (await res.text()) || res.statusText;\n throw new Error(`${res.status}: ${text}`);\n }\n}\n\nexport async function apiRequest(\n method: string,\n url: string,\n data?: unknown | undefined,\n): Promise<Response> {\n const res = await fetch(url, {\n method,\n headers: {\n ...(data ? { \"Content-Type\": \"application/json\" } : {}),\n 'Accept': 'application/json',\n },\n body: data ? JSON.stringify(data) : undefined,\n credentials: \"include\",\n keepalive: true, // Performance optimization\n });\n\n await throwIfResNotOk(res);\n return res;\n}\n\ntype UnauthorizedBehavior = \"returnNull\" | \"throw\";\nexport const getQueryFn: <T>(options: {\n on401: UnauthorizedBehavior;\n}) => QueryFunction<T> =\n ({ on401: unauthorizedBehavior }) =>\n async ({ queryKey }) => {\n const res = await fetch(queryKey.join(\"/\") as string, {\n credentials: \"include\",\n });\n\n if (unauthorizedBehavior === \"returnNull\" && res.status === 401) {\n return null;\n }\n\n await throwIfResNotOk(res);\n return await res.json();\n };\n\nexport const queryClient = new QueryClient({\n defaultOptions: {\n queries: {\n queryFn: getQueryFn({ on401: \"throw\" }),\n refetchInterval: false,\n refetchOnWindowFocus: false,\n staleTime: 5 * 60 * 1000, // 5 minutes instead of Infinity for better balance\n gcTime: 10 * 60 * 1000, // 10 minutes garbage collection time (replaces cacheTime in v5)\n retry: (failureCount, error: any) => {\n // Reduce retries for faster error handling\n return failureCount < 1 && !error?.message?.includes('401');\n },\n },\n mutations: {\n retry: false,\n },\n },\n});\n","size_bytes":1795},"client/src/lib/repair-data.ts":{"content":"// This file can be used for constants and utility functions related to repair data\n\nexport const DEVICE_TYPES = {\n LAPTOP: \"laptop\",\n DESKTOP: \"desktop\",\n} as const;\n\nexport const CATEGORIES = {\n HARDWARE: \"hardware\",\n SOFTWARE: \"software\",\n CLEANING: \"cleaning\",\n UPGRADES: \"upgrades\",\n} as const;\n\nexport const DIFFICULTY_LEVELS = {\n EASY: \"easy\",\n MEDIUM: \"medium\",\n HARD: \"hard\",\n} as const;\n\nexport const TROUBLESHOOTING_TYPES = {\n POWER: \"power\",\n PERFORMANCE: \"performance\",\n DISPLAY: \"display\",\n} as const;\n\nexport const getDifficultyColor = (difficulty: string) => {\n switch (difficulty) {\n case DIFFICULTY_LEVELS.EASY:\n return \"bg-success-50 text-success-600\";\n case DIFFICULTY_LEVELS.MEDIUM:\n return \"bg-warning-50 text-warning-700\";\n case DIFFICULTY_LEVELS.HARD:\n return \"bg-danger-50 text-danger-700\";\n default:\n return \"bg-gray-50 text-gray-600\";\n }\n};\n\nexport const formatViewCount = (count: number) => {\n if (count >= 1000) {\n return `${(count / 1000).toFixed(1)}k`;\n }\n return count.toString();\n};\n\nexport const getCategoryIcon = (category: string) => {\n switch (category) {\n case CATEGORIES.HARDWARE:\n return \"memory\";\n case CATEGORIES.SOFTWARE:\n return \"code\";\n case CATEGORIES.CLEANING:\n return \"cleaning_services\";\n case CATEGORIES.UPGRADES:\n return \"upgrade\";\n default:\n return \"build\";\n }\n};\n\nexport const getCategoryColor = (category: string) => {\n switch (category) {\n case CATEGORIES.HARDWARE:\n return \"text-primary-700\";\n case CATEGORIES.SOFTWARE:\n return \"text-success-600\";\n case CATEGORIES.CLEANING:\n return \"text-warning-700\";\n case CATEGORIES.UPGRADES:\n return \"text-purple-600\";\n default:\n return \"text-gray-600\";\n }\n};\n","size_bytes":1790},"client/src/lib/utils.ts":{"content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs))\n}\n","size_bytes":166},"client/src/pages/admin-dashboard.tsx":{"content":"import { useEffect } from \"react\";\nimport { useLocation } from \"wouter\";\nimport { useAuth } from \"@/hooks/useAuth\";\nimport { useAdminStats } from \"@/hooks/useAdmin\";\nimport { useToast } from \"@/hooks/use-toast\";\nimport Header from \"@/components/layout/header\";\nimport Footer from \"@/components/layout/footer\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { Badge } from \"@/components/ui/badge\";\n\nexport default function AdminDashboard() {\n const { user, isAuthenticated, isLoading: authLoading } = useAuth();\n const { stats, isLoading: statsLoading } = useAdminStats();\n const { toast } = useToast();\n const [, setLocation] = useLocation();\n\n // Redirect if not admin\n useEffect(() => {\n if (!authLoading && (!isAuthenticated || user?.role !== \"admin\")) {\n toast({\n title: \"Access Denied\",\n description: \"Administrator privileges required to access this page.\",\n variant: \"destructive\",\n });\n setLocation(\"/\");\n }\n }, [user, isAuthenticated, authLoading, toast, setLocation]);\n\n if (authLoading || statsLoading) {\n return (\n <div className=\"min-h-screen bg-gray-50 dark:bg-gray-900 amoled:bg-black\">\n <Header />\n <main className=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6\">\n <div className=\"animate-pulse space-y-6\">\n <div className=\"h-8 bg-gray-200 dark:bg-gray-700 rounded w-1/3\"></div>\n <div className=\"grid grid-cols-1 md:grid-cols-3 gap-6\">\n {[...Array(3)].map((_, i) => (\n <div key={i} className=\"h-32 bg-gray-200 dark:bg-gray-700 rounded-xl\"></div>\n ))}\n </div>\n </div>\n </main>\n </div>\n );\n }\n\n if (!user || user.role !== \"admin\") {\n return null; // Will redirect via useEffect\n }\n\n return (\n <div className=\"min-h-screen bg-gray-50 dark:bg-gray-900 amoled:bg-black\">\n <Header />\n \n <main className=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6\">\n {/* Header */}\n <div className=\"mb-8\">\n <div className=\"flex items-center justify-between\">\n <div>\n <h1 className=\"text-3xl font-bold text-gray-900 dark:text-white\">\n Admin Dashboard\n </h1>\n <p className=\"text-gray-600 dark:text-gray-400 mt-2\">\n Welcome back, {user.firstName || user.email}\n </p>\n </div>\n <Badge variant=\"default\" className=\"bg-green-600 text-white\">\n <i className=\"material-icons text-sm mr-1\">admin_panel_settings</i>\n Administrator\n </Badge>\n </div>\n </div>\n\n {/* Stats Cards */}\n <div className=\"grid grid-cols-1 md:grid-cols-3 gap-6 mb-8\">\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle className=\"text-sm font-medium\">Total Guides</CardTitle>\n <i className=\"material-icons text-primary-600\">library_books</i>\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold\">{stats?.totalGuides || 0}</div>\n <p className=\"text-xs text-gray-600 dark:text-gray-400\">\n Active repair guides\n </p>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle className=\"text-sm font-medium\">Total Views</CardTitle>\n <i className=\"material-icons text-blue-600\">visibility</i>\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold\">{stats?.totalViews || 0}</div>\n <p className=\"text-xs text-gray-600 dark:text-gray-400\">\n Across all guides\n </p>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle className=\"text-sm font-medium\">Total Downloads</CardTitle>\n <i className=\"material-icons text-green-600\">download</i>\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold\">{stats?.totalDownloads || 0}</div>\n <p className=\"text-xs text-gray-600 dark:text-gray-400\">\n Guide downloads\n </p>\n </CardContent>\n </Card>\n </div>\n\n {/* Admin Info */}\n <Card className=\"mb-8\">\n <CardHeader>\n <CardTitle className=\"flex items-center\">\n <i className=\"material-icons text-lg mr-2\">account_circle</i>\n Administrator Information\n </CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4 text-sm\">\n <div>\n <span className=\"font-medium text-gray-900 dark:text-white\">Email:</span>\n <span className=\"ml-2 text-gray-600 dark:text-gray-400\">{user.email}</span>\n </div>\n <div>\n <span className=\"font-medium text-gray-900 dark:text-white\">Role:</span>\n <span className=\"ml-2 text-gray-600 dark:text-gray-400\">Administrator</span>\n </div>\n <div>\n <span className=\"font-medium text-gray-900 dark:text-white\">Name:</span>\n <span className=\"ml-2 text-gray-600 dark:text-gray-400\">\n {user.firstName} {user.lastName}\n </span>\n </div>\n <div>\n <span className=\"font-medium text-gray-900 dark:text-white\">User ID:</span>\n <span className=\"ml-2 text-gray-600 dark:text-gray-400 font-mono text-xs\">\n {user.id}\n </span>\n </div>\n </div>\n </CardContent>\n </Card>\n\n {/* System Information */}\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center\">\n <i className=\"material-icons text-lg mr-2\">info</i>\n System Information\n </CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4 text-sm\">\n <div>\n <span className=\"font-medium text-gray-900 dark:text-white\">App Version:</span>\n <span className=\"ml-2 text-gray-600 dark:text-gray-400\">1.0.0</span>\n </div>\n <div>\n <span className=\"font-medium text-gray-900 dark:text-white\">Build Date:</span>\n <span className=\"ml-2 text-gray-600 dark:text-gray-400\">2025.08.18</span>\n </div>\n <div>\n <span className=\"font-medium text-gray-900 dark:text-white\">Database:</span>\n <span className=\"ml-2 text-gray-600 dark:text-gray-400\">PostgreSQL (Connected)</span>\n </div>\n <div>\n <span className=\"font-medium text-gray-900 dark:text-white\">Authentication:</span>\n <span className=\"ml-2 text-gray-600 dark:text-gray-400\">Replit Auth</span>\n </div>\n </div>\n </CardContent>\n </Card>\n </main>\n \n <Footer />\n </div>\n );\n}","size_bytes":7376},"client/src/pages/create-guide.tsx":{"content":"import React, { useState } from \"react\";\nimport { useMutation, useQueryClient } from \"@tanstack/react-query\";\nimport { useForm } from \"react-hook-form\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { z } from \"zod\";\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { Label } from \"@/components/ui/label\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ui/select\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { useToast } from \"@/hooks/use-toast\";\nimport { apiRequest } from \"@/lib/queryClient\";\nimport { Plus, X, Save, Eye } from \"lucide-react\";\nimport { useLocation } from \"wouter\";\n\nconst createGuideSchema = z.object({\n title: z.string().min(10, \"Title must be at least 10 characters\"),\n description: z.string().min(20, \"Description must be at least 20 characters\"),\n deviceType: z.enum([\"laptop\", \"desktop\"]),\n category: z.enum([\"hardware\", \"software\", \"cleaning\", \"upgrades\"]),\n difficulty: z.enum([\"easy\", \"medium\", \"hard\"]),\n estimatedTime: z.string().min(5, \"Please provide estimated time\"),\n imageUrl: z.string().url(\"Please provide a valid image URL\"),\n});\n\nconst stepSchema = z.object({\n stepNumber: z.number(),\n title: z.string().min(5, \"Step title required\"),\n description: z.string().min(10, \"Step description required\"),\n notes: z.array(z.string()).optional(),\n warnings: z.array(z.string()).optional(),\n});\n\ntype CreateGuideForm = z.infer<typeof createGuideSchema>;\ntype RepairStep = z.infer<typeof stepSchema>;\n\nexport default function CreateGuide() {\n const [, setLocation] = useLocation();\n const { toast } = useToast();\n const queryClient = useQueryClient();\n \n const [steps, setSteps] = useState<RepairStep[]>([]);\n const [toolsRequired, setToolsRequired] = useState<string[]>([]);\n const [safetyWarnings, setSafetyWarnings] = useState<string[]>([]);\n const [alternativeSolutions, setAlternativeSolutions] = useState(\"\");\n const [currentTool, setCurrentTool] = useState(\"\");\n const [currentSafety, setCurrentSafety] = useState(\"\");\n const [previewMode, setPreviewMode] = useState(false);\n\n const form = useForm<CreateGuideForm>({\n resolver: zodResolver(createGuideSchema),\n defaultValues: {\n deviceType: \"laptop\",\n category: \"hardware\",\n difficulty: \"easy\",\n },\n });\n\n const createGuideMutation = useMutation({\n mutationFn: async (data: any) => {\n return apiRequest(\"/api/user-guides\", \"POST\", data);\n },\n onSuccess: () => {\n toast({\n title: \"Success\",\n description: \"Your repair guide has been created successfully!\",\n });\n queryClient.invalidateQueries({ queryKey: [\"/api/repair-guides\"] });\n setLocation(\"/\");\n },\n onError: () => {\n toast({\n title: \"Error\",\n description: \"Failed to create repair guide. Please try again.\",\n variant: \"destructive\",\n });\n },\n });\n\n const addStep = () => {\n const newStep: RepairStep = {\n stepNumber: steps.length + 1,\n title: \"\",\n description: \"\",\n notes: [],\n warnings: [],\n };\n setSteps([...steps, newStep]);\n };\n\n const updateStep = (index: number, field: keyof RepairStep, value: any) => {\n const updatedSteps = [...steps];\n updatedSteps[index] = { ...updatedSteps[index], [field]: value };\n setSteps(updatedSteps);\n };\n\n const removeStep = (index: number) => {\n const updatedSteps = steps.filter((_, i) => i !== index);\n // Renumber remaining steps\n const renumberedSteps = updatedSteps.map((step, i) => ({\n ...step,\n stepNumber: i + 1,\n }));\n setSteps(renumberedSteps);\n };\n\n const addTool = () => {\n if (currentTool.trim() && !toolsRequired.includes(currentTool.trim())) {\n setToolsRequired([...toolsRequired, currentTool.trim()]);\n setCurrentTool(\"\");\n }\n };\n\n const addSafetyWarning = () => {\n if (currentSafety.trim() && !safetyWarnings.includes(currentSafety.trim())) {\n setSafetyWarnings([...safetyWarnings, currentSafety.trim()]);\n setCurrentSafety(\"\");\n }\n };\n\n const onSubmit = (data: CreateGuideForm) => {\n if (steps.length === 0) {\n toast({\n title: \"Error\",\n description: \"Please add at least one step to your guide.\",\n variant: \"destructive\",\n });\n return;\n }\n\n if (toolsRequired.length === 0) {\n toast({\n title: \"Error\", \n description: \"Please add at least one required tool.\",\n variant: \"destructive\",\n });\n return;\n }\n\n const guideData = {\n ...data,\n steps,\n toolsRequired,\n safetyWarnings,\n alternativeSolutions: alternativeSolutions || \"No alternative solutions provided\",\n };\n\n createGuideMutation.mutate(guideData);\n };\n\n const getDifficultyColor = (difficulty: string) => {\n switch (difficulty) {\n case \"easy\": return \"bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300\";\n case \"medium\": return \"bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300\";\n case \"hard\": return \"bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300\";\n default: return \"bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300\";\n }\n };\n\n if (previewMode) {\n const formData = form.getValues();\n return (\n <div className=\"container mx-auto py-8 px-4\">\n <div className=\"max-w-4xl mx-auto\">\n <div className=\"flex justify-between items-center mb-6\">\n <h1 className=\"text-3xl font-bold\">Guide Preview</h1>\n <Button onClick={() => setPreviewMode(false)} variant=\"outline\">\n <X className=\"h-4 w-4 mr-2\" />\n Back to Edit\n </Button>\n </div>\n\n <Card>\n <CardHeader>\n <div className=\"flex justify-between items-start\">\n <div>\n <CardTitle className=\"text-2xl\">{formData.title}</CardTitle>\n <CardDescription className=\"mt-2\">{formData.description}</CardDescription>\n </div>\n <div className=\"flex gap-2\">\n <Badge className={getDifficultyColor(formData.difficulty)}>\n {formData.difficulty}\n </Badge>\n <Badge variant=\"secondary\">{formData.category}</Badge>\n </div>\n </div>\n <div className=\"flex items-center gap-4 text-sm text-muted-foreground\">\n <span>📱 {formData.deviceType}</span>\n <span>⏱️ {formData.estimatedTime}</span>\n </div>\n </CardHeader>\n <CardContent className=\"space-y-6\">\n {formData.imageUrl && (\n <img src={formData.imageUrl} alt=\"Guide preview\" className=\"w-full h-64 object-cover rounded-lg\" />\n )}\n\n <div>\n <h3 className=\"font-semibold mb-2\">Tools Required</h3>\n <div className=\"flex flex-wrap gap-2\">\n {toolsRequired.map((tool, index) => (\n <Badge key={index} variant=\"outline\">{tool}</Badge>\n ))}\n </div>\n </div>\n\n {safetyWarnings.length > 0 && (\n <div>\n <h3 className=\"font-semibold mb-2 text-red-600\">Safety Warnings</h3>\n <ul className=\"list-disc list-inside space-y-1\">\n {safetyWarnings.map((warning, index) => (\n <li key={index} className=\"text-sm text-red-600\">{warning}</li>\n ))}\n </ul>\n </div>\n )}\n\n <div>\n <h3 className=\"font-semibold mb-4\">Repair Steps</h3>\n <div className=\"space-y-4\">\n {steps.map((step, index) => (\n <Card key={index}>\n <CardContent className=\"p-4\">\n <h4 className=\"font-semibold mb-2\">Step {step.stepNumber}: {step.title}</h4>\n <p className=\"text-sm mb-2\">{step.description}</p>\n {step.notes && step.notes.length > 0 && (\n <div className=\"text-xs text-blue-600\">\n <strong>Notes:</strong> {step.notes.join(\", \")}\n </div>\n )}\n {step.warnings && step.warnings.length > 0 && (\n <div className=\"text-xs text-red-600\">\n <strong>Warnings:</strong> {step.warnings.join(\", \")}\n </div>\n )}\n </CardContent>\n </Card>\n ))}\n </div>\n </div>\n\n {alternativeSolutions && (\n <div>\n <h3 className=\"font-semibold mb-2\">Alternative Solutions</h3>\n <p className=\"text-sm\">{alternativeSolutions}</p>\n </div>\n )}\n </CardContent>\n </Card>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"container mx-auto py-8 px-4\">\n <div className=\"max-w-4xl mx-auto\">\n <div className=\"flex justify-between items-center mb-6\">\n <div>\n <h1 className=\"text-3xl font-bold\">Create Repair Guide</h1>\n <p className=\"text-muted-foreground\">Share your repair knowledge with the community</p>\n </div>\n <div className=\"flex gap-2\">\n <Button onClick={() => setPreviewMode(true)} variant=\"outline\">\n <Eye className=\"h-4 w-4 mr-2\" />\n Preview\n </Button>\n </div>\n </div>\n\n <form onSubmit={form.handleSubmit(onSubmit)} className=\"space-y-6\">\n <Card>\n <CardHeader>\n <CardTitle>Basic Information</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div>\n <Label htmlFor=\"title\">Guide Title</Label>\n <Input\n id=\"title\"\n {...form.register(\"title\")}\n placeholder=\"e.g., How to Replace a Laptop Screen\"\n />\n {form.formState.errors.title && (\n <p className=\"text-sm text-red-600\">{form.formState.errors.title.message}</p>\n )}\n </div>\n\n <div>\n <Label htmlFor=\"description\">Description</Label>\n <Textarea\n id=\"description\"\n {...form.register(\"description\")}\n placeholder=\"Detailed description of what this guide covers...\"\n rows={3}\n />\n {form.formState.errors.description && (\n <p className=\"text-sm text-red-600\">{form.formState.errors.description.message}</p>\n )}\n </div>\n\n <div className=\"grid grid-cols-2 md:grid-cols-4 gap-4\">\n <div>\n <Label>Device Type</Label>\n <Select onValueChange={(value) => form.setValue(\"deviceType\", value as \"laptop\" | \"desktop\")}>\n <SelectTrigger>\n <SelectValue placeholder=\"Select device\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"laptop\">Laptop</SelectItem>\n <SelectItem value=\"desktop\">Desktop</SelectItem>\n </SelectContent>\n </Select>\n </div>\n\n <div>\n <Label>Category</Label>\n <Select onValueChange={(value) => form.setValue(\"category\", value as any)}>\n <SelectTrigger>\n <SelectValue placeholder=\"Select category\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"hardware\">Hardware</SelectItem>\n <SelectItem value=\"software\">Software</SelectItem>\n <SelectItem value=\"cleaning\">Cleaning</SelectItem>\n <SelectItem value=\"upgrades\">Upgrades</SelectItem>\n </SelectContent>\n </Select>\n </div>\n\n <div>\n <Label>Difficulty</Label>\n <Select onValueChange={(value) => form.setValue(\"difficulty\", value as any)}>\n <SelectTrigger>\n <SelectValue placeholder=\"Select difficulty\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"easy\">Easy</SelectItem>\n <SelectItem value=\"medium\">Medium</SelectItem>\n <SelectItem value=\"hard\">Hard</SelectItem>\n </SelectContent>\n </Select>\n </div>\n\n <div>\n <Label htmlFor=\"estimatedTime\">Time Required</Label>\n <Input\n id=\"estimatedTime\"\n {...form.register(\"estimatedTime\")}\n placeholder=\"e.g., 30-45 min\"\n />\n </div>\n </div>\n\n <div>\n <Label htmlFor=\"imageUrl\">Guide Image URL</Label>\n <Input\n id=\"imageUrl\"\n {...form.register(\"imageUrl\")}\n placeholder=\"https://example.com/image.jpg\"\n />\n {form.formState.errors.imageUrl && (\n <p className=\"text-sm text-red-600\">{form.formState.errors.imageUrl.message}</p>\n )}\n </div>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Tools & Safety</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div>\n <Label>Required Tools</Label>\n <div className=\"flex gap-2 mb-2\">\n <Input\n value={currentTool}\n onChange={(e) => setCurrentTool(e.target.value)}\n placeholder=\"Add a required tool...\"\n onKeyPress={(e) => e.key === \"Enter\" && (e.preventDefault(), addTool())}\n />\n <Button type=\"button\" onClick={addTool}>\n <Plus className=\"h-4 w-4\" />\n </Button>\n </div>\n <div className=\"flex flex-wrap gap-2\">\n {toolsRequired.map((tool, index) => (\n <Badge key={index} variant=\"outline\" className=\"cursor-pointer\" onClick={() => setToolsRequired(toolsRequired.filter((_, i) => i !== index))}>\n {tool} <X className=\"h-3 w-3 ml-1\" />\n </Badge>\n ))}\n </div>\n </div>\n\n <div>\n <Label>Safety Warnings</Label>\n <div className=\"flex gap-2 mb-2\">\n <Input\n value={currentSafety}\n onChange={(e) => setCurrentSafety(e.target.value)}\n placeholder=\"Add a safety warning...\"\n onKeyPress={(e) => e.key === \"Enter\" && (e.preventDefault(), addSafetyWarning())}\n />\n <Button type=\"button\" onClick={addSafetyWarning}>\n <Plus className=\"h-4 w-4\" />\n </Button>\n </div>\n <div className=\"flex flex-wrap gap-2\">\n {safetyWarnings.map((warning, index) => (\n <Badge key={index} variant=\"destructive\" className=\"cursor-pointer\" onClick={() => setSafetyWarnings(safetyWarnings.filter((_, i) => i !== index))}>\n {warning} <X className=\"h-3 w-3 ml-1\" />\n </Badge>\n ))}\n </div>\n </div>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <div className=\"flex justify-between items-center\">\n <CardTitle>Repair Steps</CardTitle>\n <Button type=\"button\" onClick={addStep}>\n <Plus className=\"h-4 w-4 mr-2\" />\n Add Step\n </Button>\n </div>\n </CardHeader>\n <CardContent>\n <div className=\"space-y-4\">\n {steps.map((step, index) => (\n <Card key={index}>\n <CardContent className=\"p-4\">\n <div className=\"flex justify-between items-center mb-2\">\n <h4 className=\"font-semibold\">Step {step.stepNumber}</h4>\n <Button type=\"button\" variant=\"destructive\" size=\"sm\" onClick={() => removeStep(index)}>\n <X className=\"h-4 w-4\" />\n </Button>\n </div>\n <div className=\"space-y-2\">\n <Input\n placeholder=\"Step title...\"\n value={step.title}\n onChange={(e) => updateStep(index, \"title\", e.target.value)}\n />\n <Textarea\n placeholder=\"Step description...\"\n value={step.description}\n onChange={(e) => updateStep(index, \"description\", e.target.value)}\n rows={2}\n />\n </div>\n </CardContent>\n </Card>\n ))}\n {steps.length === 0 && (\n <div className=\"text-center py-8 text-muted-foreground\">\n <p>No steps added yet. Click \"Add Step\" to get started.</p>\n </div>\n )}\n </div>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Additional Information</CardTitle>\n </CardHeader>\n <CardContent>\n <div>\n <Label htmlFor=\"alternatives\">Alternative Solutions (Optional)</Label>\n <Textarea\n id=\"alternatives\"\n value={alternativeSolutions}\n onChange={(e) => setAlternativeSolutions(e.target.value)}\n placeholder=\"Any alternative approaches or solutions...\"\n rows={3}\n />\n </div>\n </CardContent>\n </Card>\n\n <div className=\"flex justify-end gap-4\">\n <Button type=\"button\" variant=\"outline\" onClick={() => setLocation(\"/\")}>\n Cancel\n </Button>\n <Button type=\"submit\" disabled={createGuideMutation.isPending}>\n <Save className=\"h-4 w-4 mr-2\" />\n {createGuideMutation.isPending ? \"Creating...\" : \"Create Guide\"}\n </Button>\n </div>\n </form>\n </div>\n </div>\n );\n}","size_bytes":19284},"client/src/pages/device-finder.tsx":{"content":"import React, { useState } from \"react\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { useLocation } from \"wouter\";\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Search, Laptop, Monitor, Star, Wrench, Filter, ExternalLink, Eye, Download } from \"lucide-react\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@/components/ui/tabs\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ui/select\";\nimport { Separator } from \"@/components/ui/separator\";\nimport Header from \"@/components/layout/header\";\nimport Footer from \"@/components/layout/footer\";\nimport type { DeviceBrand, DeviceModel, RepairGuide } from \"@shared/schema\";\n\nexport default function DeviceFinder() {\n const [, setLocation] = useLocation();\n const [selectedDeviceType, setSelectedDeviceType] = useState<\"laptop\" | \"desktop\">(\"laptop\");\n const [selectedBrand, setSelectedBrand] = useState<string>(\"\");\n const [selectedModel, setSelectedModel] = useState<string>(\"\");\n const [searchQuery, setSearchQuery] = useState(\"\");\n const [guideFilter, setGuideFilter] = useState<string>(\"all\");\n const [categoryFilter, setCategoryFilter] = useState<string>(\"all\");\n\n // Fetch brands for selected device type\n const { data: brands = [] } = useQuery<DeviceBrand[]>({\n queryKey: [\"/api/device-brands\", selectedDeviceType],\n queryFn: () => fetch(`/api/device-brands?deviceType=${selectedDeviceType}`).then(res => res.json()),\n });\n\n // Fetch models for selected brand\n const { data: models = [] } = useQuery<DeviceModel[]>({\n queryKey: [\"/api/device-models\", selectedBrand],\n queryFn: () => selectedBrand ? fetch(`/api/brands/${selectedBrand}/models`).then(res => res.json()) : Promise.resolve([]),\n enabled: !!selectedBrand,\n });\n\n // Fetch compatible guides for selected model\n const { data: compatibleGuides = [] } = useQuery<RepairGuide[]>({\n queryKey: [\"/api/models\", selectedModel, \"guides\"],\n queryFn: () => selectedModel ? fetch(`/api/models/${selectedModel}/guides`).then(res => res.json()) : Promise.resolve([]),\n enabled: !!selectedModel,\n });\n\n // Search models when typing\n const { data: searchResults = [] } = useQuery<DeviceModel[]>({\n queryKey: [\"/api/device-models/search\", searchQuery, selectedDeviceType],\n queryFn: () => searchQuery ? fetch(`/api/device-models?search=${encodeURIComponent(searchQuery)}&deviceType=${selectedDeviceType}`).then(res => res.json()) : Promise.resolve([]),\n enabled: searchQuery.length > 2,\n });\n\n const selectedModelData = models.find(m => m.id === selectedModel);\n\n const getSupportLevelColor = (level: string) => {\n switch (level) {\n case \"official\": return \"bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300\";\n case \"community\": return \"bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300\";\n case \"basic\": return \"bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300\";\n default: return \"bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300\";\n }\n };\n\n // Filter compatible guides based on selected filters\n const filteredGuides = compatibleGuides.filter(guide => {\n const difficultyMatch = guideFilter === \"all\" || guide.difficulty === guideFilter;\n const categoryMatch = categoryFilter === \"all\" || guide.category === categoryFilter;\n return difficultyMatch && categoryMatch;\n });\n\n const handleGuideClick = (guideId: string) => {\n setLocation(`/guide/${guideId}`);\n };\n\n return (\n <div className=\"min-h-screen bg-gray-50 dark:bg-gray-900 amoled:bg-black\">\n <Header />\n \n <div className=\"container mx-auto py-8 px-4\">\n <div className=\"max-w-6xl mx-auto\">\n <div className=\"text-center mb-8\">\n <h1 className=\"text-4xl font-bold mb-4 text-gray-900 dark:text-white amoled:text-white\">\n Device Finder\n </h1>\n <p className=\"text-xl text-gray-600 dark:text-gray-300 amoled:text-gray-300\">\n Find specific repair guides for your laptop or desktop computer model\n </p>\n <div className=\"mt-6 flex flex-wrap items-center justify-center gap-4 text-sm text-gray-500 dark:text-gray-400 amoled:text-gray-400\">\n <div className=\"flex items-center gap-2\">\n <Laptop className=\"h-4 w-4 text-primary-600\" />\n <span>25+ Device Brands</span>\n </div>\n <Separator orientation=\"vertical\" className=\"h-4\" />\n <div className=\"flex items-center gap-2\">\n <Monitor className=\"h-4 w-4 text-primary-600\" />\n <span>150+ Device Models</span>\n </div>\n <Separator orientation=\"vertical\" className=\"h-4\" />\n <div className=\"flex items-center gap-2\">\n <Wrench className=\"h-4 w-4 text-primary-600\" />\n <span>Compatibility Verified</span>\n </div>\n </div>\n </div>\n\n <Tabs value={selectedDeviceType} onValueChange={(value) => {\n setSelectedDeviceType(value as \"laptop\" | \"desktop\");\n setSelectedBrand(\"\");\n setSelectedModel(\"\");\n setSearchQuery(\"\");\n }} className=\"mb-8\">\n <TabsList className=\"grid w-full grid-cols-2\">\n <TabsTrigger value=\"laptop\" className=\"flex items-center gap-2\">\n <Laptop className=\"h-4 w-4\" />\n Laptops\n </TabsTrigger>\n <TabsTrigger value=\"desktop\" className=\"flex items-center gap-2\">\n <Monitor className=\"h-4 w-4\" />\n Desktops\n </TabsTrigger>\n </TabsList>\n\n <TabsContent value={selectedDeviceType} className=\"space-y-6\">\n {/* Search Section */}\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <Search className=\"h-5 w-5\" />\n Quick Search\n </CardTitle>\n <CardDescription>\n Search for your specific device model\n </CardDescription>\n </CardHeader>\n <CardContent>\n <div className=\"relative\">\n <Input\n placeholder={`Search ${selectedDeviceType} models...`}\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n className=\"pr-10\"\n />\n <Search className=\"absolute right-3 top-3 h-4 w-4 text-muted-foreground\" />\n </div>\n \n {searchResults.length > 0 && (\n <div className=\"mt-4 grid gap-3\">\n {searchResults.slice(0, 5).map((model) => (\n <Card key={model.id} className=\"cursor-pointer hover:bg-accent\" onClick={() => {\n setSelectedModel(model.id);\n setSelectedBrand(model.brandId);\n setSearchQuery(\"\");\n }}>\n <CardContent className=\"p-4\">\n <div className=\"flex justify-between items-center\">\n <div>\n <h4 className=\"font-semibold\">{model.name}</h4>\n <p className=\"text-sm text-muted-foreground\">{model.series} • {model.year}</p>\n </div>\n <Badge variant=\"secondary\">{model.guideCount} guides</Badge>\n </div>\n </CardContent>\n </Card>\n ))}\n </div>\n )}\n </CardContent>\n </Card>\n\n {/* Brand Selection */}\n <Card>\n <CardHeader>\n <CardTitle>Browse by Brand</CardTitle>\n <CardDescription>\n Select your device brand to see compatible models\n </CardDescription>\n </CardHeader>\n <CardContent>\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-3\">\n {brands.map((brand) => (\n <Card\n key={brand.id}\n className={`cursor-pointer transition-colors ${\n selectedBrand === brand.id ? \"ring-2 ring-primary\" : \"hover:bg-accent\"\n }`}\n onClick={() => {\n setSelectedBrand(brand.id);\n setSelectedModel(\"\");\n }}\n >\n <CardContent className=\"p-4\">\n <div className=\"flex justify-between items-center mb-2\">\n <h3 className=\"font-semibold\">{brand.name}</h3>\n <Badge className={getSupportLevelColor(brand.supportLevel)}>\n {brand.supportLevel}\n </Badge>\n </div>\n <div className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n <Star className=\"h-4 w-4\" />\n {brand.popularity} popularity\n </div>\n </CardContent>\n </Card>\n ))}\n </div>\n </CardContent>\n </Card>\n\n {/* Model Selection */}\n {selectedBrand && models.length > 0 && (\n <Card>\n <CardHeader>\n <CardTitle>Select Your Model</CardTitle>\n <CardDescription>\n Choose your specific device model\n </CardDescription>\n </CardHeader>\n <CardContent>\n <div className=\"grid gap-3 md:grid-cols-2\">\n {models.map((model) => (\n <Card\n key={model.id}\n className={`cursor-pointer transition-colors ${\n selectedModel === model.id ? \"ring-2 ring-primary\" : \"hover:bg-accent\"\n }`}\n onClick={() => setSelectedModel(model.id)}\n >\n <CardContent className=\"p-4\">\n <div className=\"flex justify-between items-center mb-2\">\n <h4 className=\"font-semibold\">{model.name}</h4>\n <Badge variant=\"outline\">{model.year}</Badge>\n </div>\n <p className=\"text-sm text-muted-foreground mb-2\">{model.series}</p>\n <div className=\"flex items-center gap-2 text-sm\">\n <Wrench className=\"h-4 w-4\" />\n {model.guideCount} repair guides\n </div>\n </CardContent>\n </Card>\n ))}\n </div>\n </CardContent>\n </Card>\n )}\n\n {/* Model Details and Compatible Guides */}\n {selectedModelData && (\n <div className=\"grid gap-6 lg:grid-cols-3\">\n {/* Model Details */}\n <Card className=\"lg:col-span-1\">\n <CardHeader>\n <div className=\"flex items-center justify-between\">\n <div>\n <CardTitle className=\"text-lg\">{selectedModelData.name}</CardTitle>\n <CardDescription>\n {selectedModelData.series} • {selectedModelData.year}\n </CardDescription>\n </div>\n <Badge variant=\"outline\" className=\"bg-primary-50 dark:bg-primary-900 text-primary-700 dark:text-primary-300\">\n {selectedModelData.guideCount} guides\n </Badge>\n </div>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n {selectedModelData.specifications && (\n <div className=\"space-y-3\">\n <h4 className=\"font-semibold text-sm text-gray-900 dark:text-white amoled:text-white flex items-center gap-2\">\n <i className=\"material-icons text-sm\">computer</i>\n Specifications\n </h4>\n <div className=\"text-sm space-y-2 bg-gray-50 dark:bg-gray-800 amoled:bg-gray-900 p-3 rounded-lg\">\n {Object.entries(selectedModelData.specifications as Record<string, any>).map(([key, value]) => (\n <div key={key} className=\"flex justify-between items-center\">\n <span className=\"capitalize text-gray-600 dark:text-gray-400 amoled:text-gray-400 font-medium\">\n {key}:\n </span>\n <span className=\"text-gray-900 dark:text-white amoled:text-white\">{String(value)}</span>\n </div>\n ))}\n </div>\n </div>\n )}\n \n {selectedModelData.commonIssues && (selectedModelData.commonIssues as any[]).length > 0 && (\n <div className=\"space-y-3\">\n <h4 className=\"font-semibold text-sm text-gray-900 dark:text-white amoled:text-white flex items-center gap-2\">\n <i className=\"material-icons text-sm text-red-600\">warning</i>\n Common Issues\n </h4>\n <div className=\"flex flex-wrap gap-2\">\n {(selectedModelData.commonIssues as any[]).map((issue, index) => (\n <Badge key={index} variant=\"destructive\" className=\"text-xs\">\n {String(issue)}\n </Badge>\n ))}\n </div>\n </div>\n )}\n </CardContent>\n </Card>\n\n {/* Compatible Guides */}\n <Card className=\"lg:col-span-2\">\n <CardHeader>\n <div className=\"flex items-center justify-between\">\n <div>\n <CardTitle className=\"flex items-center gap-2\">\n <Wrench className=\"h-5 w-5 text-primary-600\" />\n Compatible Repair Guides\n </CardTitle>\n <CardDescription>\n {filteredGuides.length} of {compatibleGuides.length} guides match your filters\n </CardDescription>\n </div>\n \n {/* Guide Filters */}\n <div className=\"flex items-center gap-2\">\n <Select value={categoryFilter} onValueChange={setCategoryFilter}>\n <SelectTrigger className=\"w-36\">\n <Filter className=\"h-4 w-4 mr-1\" />\n <SelectValue placeholder=\"Category\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">All Categories</SelectItem>\n <SelectItem value=\"hardware\">Hardware</SelectItem>\n <SelectItem value=\"software\">Software</SelectItem>\n <SelectItem value=\"cleaning\">Cleaning</SelectItem>\n <SelectItem value=\"upgrades\">Upgrades</SelectItem>\n </SelectContent>\n </Select>\n \n <Select value={guideFilter} onValueChange={setGuideFilter}>\n <SelectTrigger className=\"w-32\">\n <SelectValue placeholder=\"Difficulty\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">All Levels</SelectItem>\n <SelectItem value=\"easy\">Easy</SelectItem>\n <SelectItem value=\"medium\">Medium</SelectItem>\n <SelectItem value=\"hard\">Hard</SelectItem>\n </SelectContent>\n </Select>\n </div>\n </div>\n </CardHeader>\n <CardContent>\n <div className=\"space-y-3 max-h-96 overflow-y-auto\">\n {filteredGuides.map((guide) => (\n <Card \n key={guide.id} \n className=\"hover:bg-gray-50 dark:hover:bg-gray-800 amoled:hover:bg-gray-900 cursor-pointer border-l-4 border-l-transparent hover:border-l-primary-500 transition-all duration-200\"\n onClick={() => handleGuideClick(guide.id)}\n >\n <CardContent className=\"p-4\">\n <div className=\"flex justify-between items-start mb-2\">\n <h5 className=\"font-semibold text-gray-900 dark:text-white amoled:text-white text-sm\">\n {guide.title}\n </h5>\n <div className=\"flex items-center gap-2\">\n <Badge \n variant={\n guide.difficulty === \"easy\" ? \"default\" :\n guide.difficulty === \"medium\" ? \"secondary\" : \"destructive\"\n }\n className=\"text-xs\"\n >\n {guide.difficulty}\n </Badge>\n <Badge variant=\"outline\" className=\"text-xs\">\n {guide.category}\n </Badge>\n </div>\n </div>\n <p className=\"text-sm text-gray-600 dark:text-gray-300 amoled:text-gray-300 mb-3 line-clamp-2\">\n {guide.description}\n </p>\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-4 text-xs text-gray-500 dark:text-gray-400 amoled:text-gray-400\">\n <span className=\"flex items-center gap-1\">\n <i className=\"material-icons text-xs\">schedule</i>\n {guide.estimatedTime}\n </span>\n <span className=\"flex items-center gap-1\">\n <Eye className=\"h-3 w-3\" />\n {guide.viewCount}\n </span>\n <span className=\"flex items-center gap-1\">\n <Download className=\"h-3 w-3\" />\n {guide.downloadCount}\n </span>\n </div>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n className=\"h-7 px-2 text-primary-600 hover:text-primary-700 hover:bg-primary-50 dark:hover:bg-primary-900\"\n onClick={(e) => {\n e.stopPropagation();\n handleGuideClick(guide.id);\n }}\n >\n <ExternalLink className=\"h-3 w-3 mr-1\" />\n View\n </Button>\n </div>\n </CardContent>\n </Card>\n ))}\n \n {filteredGuides.length === 0 && compatibleGuides.length > 0 && (\n <div className=\"text-center py-8 text-gray-500 dark:text-gray-400 amoled:text-gray-400\">\n <Filter className=\"h-12 w-12 mx-auto mb-4 opacity-50\" />\n <p className=\"font-medium\">No guides match your current filters</p>\n <p className=\"text-sm\">Try adjusting the difficulty or category filters above</p>\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"mt-3\"\n onClick={() => {\n setGuideFilter(\"all\");\n setCategoryFilter(\"all\");\n }}\n >\n Clear Filters\n </Button>\n </div>\n )}\n \n {compatibleGuides.length === 0 && (\n <div className=\"text-center py-8 text-gray-500 dark:text-gray-400 amoled:text-gray-400\">\n <Wrench className=\"h-12 w-12 mx-auto mb-4 opacity-50\" />\n <p className=\"font-medium\">No guides available yet for this model</p>\n <p className=\"text-sm\">Check back later or browse our general repair guides</p>\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"mt-3\"\n onClick={() => setLocation(\"/\")}\n >\n Browse All Guides\n </Button>\n </div>\n )}\n </div>\n </CardContent>\n </Card>\n </div>\n )}\n </TabsContent>\n </Tabs>\n\n {/* Popular Devices Section */}\n <div className=\"mt-12 text-center\">\n <h2 className=\"text-2xl font-bold mb-4 text-gray-900 dark:text-white amoled:text-white\">\n Popular Device Categories\n </h2>\n <div className=\"grid md:grid-cols-2 lg:grid-cols-4 gap-4\">\n <Card className=\"hover:bg-gray-50 dark:hover:bg-gray-800 amoled:hover:bg-gray-900 cursor-pointer transition-colors\">\n <CardContent className=\"p-6 text-center\">\n <div className=\"w-12 h-12 bg-blue-100 dark:bg-blue-900 rounded-lg flex items-center justify-center mx-auto mb-3\">\n <i className=\"material-icons text-blue-600 dark:text-blue-300\">laptop_mac</i>\n </div>\n <h3 className=\"font-semibold text-gray-900 dark:text-white amoled:text-white mb-1\">MacBook Series</h3>\n <p className=\"text-sm text-gray-600 dark:text-gray-300 amoled:text-gray-300\">15+ models supported</p>\n </CardContent>\n </Card>\n\n <Card className=\"hover:bg-gray-50 dark:hover:bg-gray-800 amoled:hover:bg-gray-900 cursor-pointer transition-colors\">\n <CardContent className=\"p-6 text-center\">\n <div className=\"w-12 h-12 bg-red-100 dark:bg-red-900 rounded-lg flex items-center justify-center mx-auto mb-3\">\n <i className=\"material-icons text-red-600 dark:text-red-300\">laptop_windows</i>\n </div>\n <h3 className=\"font-semibold text-gray-900 dark:text-white amoled:text-white mb-1\">ThinkPad Series</h3>\n <p className=\"text-sm text-gray-600 dark:text-gray-300 amoled:text-gray-300\">20+ models supported</p>\n </CardContent>\n </Card>\n\n <Card className=\"hover:bg-gray-50 dark:hover:bg-gray-800 amoled:hover:bg-gray-900 cursor-pointer transition-colors\">\n <CardContent className=\"p-6 text-center\">\n <div className=\"w-12 h-12 bg-green-100 dark:bg-green-900 rounded-lg flex items-center justify-center mx-auto mb-3\">\n <i className=\"material-icons text-green-600 dark:text-green-300\">desktop_windows</i>\n </div>\n <h3 className=\"font-semibold text-gray-900 dark:text-white amoled:text-white mb-1\">Gaming PCs</h3>\n <p className=\"text-sm text-gray-600 dark:text-gray-300 amoled:text-gray-300\">Custom builds supported</p>\n </CardContent>\n </Card>\n\n <Card className=\"hover:bg-gray-50 dark:hover:bg-gray-800 amoled:hover:bg-gray-900 cursor-pointer transition-colors\">\n <CardContent className=\"p-6 text-center\">\n <div className=\"w-12 h-12 bg-purple-100 dark:bg-purple-900 rounded-lg flex items-center justify-center mx-auto mb-3\">\n <i className=\"material-icons text-purple-600 dark:text-purple-300\">business</i>\n </div>\n <h3 className=\"font-semibold text-gray-900 dark:text-white amoled:text-white mb-1\">Business Laptops</h3>\n <p className=\"text-sm text-gray-600 dark:text-gray-300 amoled:text-gray-300\">Enterprise solutions</p>\n </CardContent>\n </Card>\n </div>\n </div>\n </div>\n </div>\n \n <Footer />\n </div>\n );\n}","size_bytes":26657},"client/src/pages/downloads.tsx":{"content":"import { useState, useEffect } from \"react\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport Header from \"@/components/layout/header\";\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { Button } from \"@/components/ui/button\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@/components/ui/tabs\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ui/select\";\nimport { Input } from \"@/components/ui/input\";\nimport { Progress } from \"@/components/ui/progress\";\nimport { useToast } from \"@/hooks/use-toast\";\nimport { \n Download, \n FileText, \n Video, \n HardDrive, \n Search,\n Filter,\n Package,\n ExternalLink,\n Archive,\n Trash2,\n CheckCircle,\n AlertCircle,\n Smartphone\n} from \"lucide-react\";\nimport { downloadGuide, downloadGuidePackage, getEstimatedSize, type DownloadOptions } from \"@/lib/download-utils\";\nimport type { RepairGuide, TroubleshootingFlow, DeviceComponent } from \"@shared/schema\";\n\ninterface DownloadableItem {\n id: string;\n title: string;\n type: 'guide' | 'troubleshooting' | 'component' | 'package';\n size: string;\n format: string;\n description?: string;\n category?: string;\n difficulty?: string;\n estimatedTime?: string;\n deviceType?: string;\n data?: any;\n}\n\nexport default function Downloads() {\n const [searchQuery, setSearchQuery] = useState(\"\");\n const [selectedCategory, setSelectedCategory] = useState(\"all\");\n const [selectedDifficulty, setSelectedDifficulty] = useState(\"all\");\n const [selectedDeviceType, setSelectedDeviceType] = useState(\"all\");\n const [selectedFormat, setSelectedFormat] = useState(\"html\");\n const [downloadProgress, setDownloadProgress] = useState<{ [key: string]: number }>({});\n const [downloadedItems, setDownloadedItems] = useState<string[]>([]);\n const { toast } = useToast();\n\n const { data: repairGuides = [], isLoading: guidesLoading } = useQuery<RepairGuide[]>({\n queryKey: [\"/api/repair-guides\"],\n });\n\n const { data: troubleshootingFlows = [], isLoading: flowsLoading } = useQuery<TroubleshootingFlow[]>({\n queryKey: [\"/api/troubleshooting-flows\"],\n });\n\n const { data: deviceComponents = [], isLoading: componentsLoading } = useQuery<DeviceComponent[]>({\n queryKey: [\"/api/device-components\"],\n });\n\n // Convert data to downloadable items\n const downloadableItems: DownloadableItem[] = [\n ...repairGuides.map(guide => ({\n id: guide.id,\n title: guide.title,\n type: 'guide' as const,\n size: getEstimatedSize(guide, { format: selectedFormat as any }),\n format: selectedFormat.toUpperCase(),\n description: guide.description,\n category: guide.category,\n difficulty: guide.difficulty,\n estimatedTime: guide.estimatedTime,\n deviceType: guide.deviceType,\n data: guide\n })),\n ...troubleshootingFlows.map(flow => ({\n id: flow.id,\n title: flow.title,\n type: 'troubleshooting' as const,\n size: '5-15 KB',\n format: 'JSON',\n description: flow.description,\n category: flow.type,\n data: flow\n })),\n ...deviceComponents.map(component => ({\n id: component.id,\n title: component.name,\n type: 'component' as const,\n size: '2-8 KB',\n format: 'JSON',\n description: component.description,\n category: component.category,\n deviceType: component.deviceType,\n data: component\n }))\n ];\n\n // Filter downloadable items\n const filteredItems = downloadableItems.filter(item => {\n const matchesSearch = item.title.toLowerCase().includes(searchQuery.toLowerCase()) ||\n item.description?.toLowerCase().includes(searchQuery.toLowerCase());\n const matchesCategory = selectedCategory === \"all\" || item.category === selectedCategory;\n const matchesDifficulty = selectedDifficulty === \"all\" || item.difficulty === selectedDifficulty;\n const matchesDeviceType = selectedDeviceType === \"all\" || item.deviceType === selectedDeviceType;\n \n return matchesSearch && matchesCategory && matchesDifficulty && matchesDeviceType;\n });\n\n // Handle individual download\n const handleDownload = async (item: DownloadableItem) => {\n try {\n setDownloadProgress(prev => ({ ...prev, [item.id]: 0 }));\n \n // Simulate progress\n const progressInterval = setInterval(() => {\n setDownloadProgress(prev => {\n const current = prev[item.id] || 0;\n if (current >= 100) {\n clearInterval(progressInterval);\n return prev;\n }\n return { ...prev, [item.id]: current + 10 };\n });\n }, 100);\n\n let success = false;\n\n if (item.type === 'guide') {\n const options: DownloadOptions = {\n format: selectedFormat as any,\n includeImages: true\n };\n success = await downloadGuide(item.data, options);\n } else {\n // For troubleshooting and components, download as JSON\n const content = JSON.stringify(item.data, null, 2);\n const blob = new Blob([content], { type: 'application/json' });\n const url = URL.createObjectURL(blob);\n \n const link = document.createElement('a');\n link.href = url;\n link.download = `${item.title.replace(/[^a-z0-9]/gi, '_').toLowerCase()}.json`;\n document.body.appendChild(link);\n link.click();\n document.body.removeChild(link);\n URL.revokeObjectURL(url);\n success = true;\n }\n\n clearInterval(progressInterval);\n setDownloadProgress(prev => ({ ...prev, [item.id]: 100 }));\n\n if (success) {\n setDownloadedItems(prev => [...prev, item.id]);\n toast({\n title: \"Download Complete\",\n description: `${item.title} has been downloaded successfully.`,\n });\n } else {\n throw new Error(\"Download failed\");\n }\n\n // Clear progress after 2 seconds\n setTimeout(() => {\n setDownloadProgress(prev => {\n const { [item.id]: _, ...rest } = prev;\n return rest;\n });\n }, 2000);\n\n } catch (error) {\n console.error('Download error:', error);\n setDownloadProgress(prev => {\n const { [item.id]: _, ...rest } = prev;\n return rest;\n });\n toast({\n title: \"Download Failed\",\n description: `Failed to download ${item.title}. Please try again.`,\n variant: \"destructive\",\n });\n }\n };\n\n // Handle package download\n const handlePackageDownload = async (category?: string, deviceType?: string) => {\n try {\n let guidesToDownload = repairGuides;\n \n if (category && category !== \"all\") {\n guidesToDownload = guidesToDownload.filter(guide => guide.category === category);\n }\n \n if (deviceType && deviceType !== \"all\") {\n guidesToDownload = guidesToDownload.filter(guide => guide.deviceType === deviceType);\n }\n\n if (guidesToDownload.length === 0) {\n toast({\n title: \"No Guides Found\",\n description: \"No guides match the selected criteria.\",\n variant: \"destructive\",\n });\n return;\n }\n\n const packageName = category && category !== \"all\" \n ? `${category}_guides_${deviceType || 'all'}`\n : `all_repair_guides`;\n\n const success = await downloadGuidePackage(guidesToDownload, packageName);\n\n if (success) {\n toast({\n title: \"Package Download Complete\",\n description: `Downloaded ${guidesToDownload.length} guides successfully.`,\n });\n }\n } catch (error) {\n console.error('Package download error:', error);\n toast({\n title: \"Package Download Failed\",\n description: \"Failed to download guide package. Please try again.\",\n variant: \"destructive\",\n });\n }\n };\n\n // Handle complete export\n const handleCompleteExport = async () => {\n try {\n const bundle = {\n exportInfo: {\n title: 'JCR Guide Pro Complete Export',\n version: '1.0',\n exportedAt: new Date().toISOString(),\n totalItems: repairGuides.length + troubleshootingFlows.length + deviceComponents.length\n },\n repairGuides: repairGuides,\n troubleshootingFlows: troubleshootingFlows,\n deviceComponents: deviceComponents\n };\n\n const content = JSON.stringify(bundle, null, 2);\n const blob = new Blob([content], { type: 'application/json' });\n const url = URL.createObjectURL(blob);\n \n const link = document.createElement('a');\n link.href = url;\n link.download = `jcr_guide_pro_complete_export_${new Date().toISOString().split('T')[0]}.json`;\n document.body.appendChild(link);\n link.click();\n document.body.removeChild(link);\n URL.revokeObjectURL(url);\n\n toast({\n title: \"Complete Export Successful\",\n description: `Exported ${bundle.exportInfo.totalItems} items to device storage.`,\n });\n } catch (error) {\n console.error('Complete export error:', error);\n toast({\n title: \"Export Failed\",\n description: \"Failed to create complete export. Please try again.\",\n variant: \"destructive\",\n });\n }\n };\n\n const getTypeIcon = (type: string) => {\n switch (type) {\n case 'guide': return <FileText className=\"h-4 w-4\" />;\n case 'troubleshooting': return <AlertCircle className=\"h-4 w-4\" />;\n case 'component': return <HardDrive className=\"h-4 w-4\" />;\n default: return <FileText className=\"h-4 w-4\" />;\n }\n };\n\n const getCategoryColor = (category?: string) => {\n switch (category) {\n case 'hardware': return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300';\n case 'software': return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300';\n case 'cleaning': return 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300';\n case 'upgrades': return 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-300';\n case 'power': return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300';\n case 'performance': return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300';\n case 'display': return 'bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-300';\n default: return 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300';\n }\n };\n\n const getDifficultyColor = (difficulty?: string) => {\n switch (difficulty) {\n case 'easy': return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300';\n case 'medium': return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300';\n case 'hard': return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300';\n default: return 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300';\n }\n };\n\n return (\n <div className=\"min-h-screen bg-background\">\n <Header />\n \n <main className=\"container mx-auto px-4 py-8\">\n <div className=\"max-w-6xl mx-auto\">\n {/* Header */}\n <div className=\"mb-8\">\n <h1 className=\"text-3xl font-bold mb-4\">Download Center</h1>\n <p className=\"text-muted-foreground mb-6\">\n Download repair guides, troubleshooting flows, and component information for offline access\n </p>\n\n {/* Quick Export Options */}\n <div className=\"flex flex-wrap gap-4 mb-6\">\n <Button onClick={handleCompleteExport} size=\"lg\" className=\"bg-gradient-to-r from-blue-600 to-blue-700\">\n <Package className=\"mr-2 h-4 w-4\" />\n Export Everything\n </Button>\n <Button \n onClick={() => handlePackageDownload(\"hardware\")} \n variant=\"outline\"\n >\n <Download className=\"mr-2 h-4 w-4\" />\n Hardware Guides\n </Button>\n <Button \n onClick={() => handlePackageDownload(\"software\")} \n variant=\"outline\"\n >\n <Download className=\"mr-2 h-4 w-4\" />\n Software Guides\n </Button>\n <Button \n onClick={() => handlePackageDownload(\"cleaning\")} \n variant=\"outline\"\n >\n <Download className=\"mr-2 h-4 w-4\" />\n Cleaning Guides\n </Button>\n </div>\n </div>\n\n {/* Filters */}\n <Card className=\"mb-6\">\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <Filter className=\"h-5 w-5\" />\n Filter Downloads\n </CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4\">\n <div>\n <label className=\"text-sm font-medium mb-2 block\">Search</label>\n <div className=\"relative\">\n <Search className=\"absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground\" />\n <Input\n placeholder=\"Search guides...\"\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n className=\"pl-10\"\n />\n </div>\n </div>\n\n <div>\n <label className=\"text-sm font-medium mb-2 block\">Category</label>\n <Select value={selectedCategory} onValueChange={setSelectedCategory}>\n <SelectTrigger>\n <SelectValue placeholder=\"All categories\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">All Categories</SelectItem>\n <SelectItem value=\"hardware\">Hardware</SelectItem>\n <SelectItem value=\"software\">Software</SelectItem>\n <SelectItem value=\"cleaning\">Cleaning</SelectItem>\n <SelectItem value=\"upgrades\">Upgrades</SelectItem>\n <SelectItem value=\"power\">Power Issues</SelectItem>\n <SelectItem value=\"performance\">Performance</SelectItem>\n <SelectItem value=\"display\">Display</SelectItem>\n </SelectContent>\n </Select>\n </div>\n\n <div>\n <label className=\"text-sm font-medium mb-2 block\">Difficulty</label>\n <Select value={selectedDifficulty} onValueChange={setSelectedDifficulty}>\n <SelectTrigger>\n <SelectValue placeholder=\"All difficulties\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">All Levels</SelectItem>\n <SelectItem value=\"easy\">Easy</SelectItem>\n <SelectItem value=\"medium\">Medium</SelectItem>\n <SelectItem value=\"hard\">Hard</SelectItem>\n </SelectContent>\n </Select>\n </div>\n\n <div>\n <label className=\"text-sm font-medium mb-2 block\">Device Type</label>\n <Select value={selectedDeviceType} onValueChange={setSelectedDeviceType}>\n <SelectTrigger>\n <SelectValue placeholder=\"All devices\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">All Devices</SelectItem>\n <SelectItem value=\"laptop\">Laptop</SelectItem>\n <SelectItem value=\"desktop\">Desktop</SelectItem>\n </SelectContent>\n </Select>\n </div>\n\n <div>\n <label className=\"text-sm font-medium mb-2 block\">Download Format</label>\n <Select value={selectedFormat} onValueChange={setSelectedFormat}>\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"html\">HTML (Recommended)</SelectItem>\n <SelectItem value=\"json\">JSON Data</SelectItem>\n <SelectItem value=\"pdf\">PDF (Printable)</SelectItem>\n </SelectContent>\n </Select>\n </div>\n </div>\n </CardContent>\n </Card>\n\n {/* Download Items */}\n {guidesLoading || flowsLoading || componentsLoading ? (\n <div className=\"text-center py-8\">Loading downloadable content...</div>\n ) : (\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6\">\n {filteredItems.map((item) => (\n <Card key={item.id} className=\"hover:shadow-lg transition-shadow\">\n <CardHeader className=\"pb-3\">\n <div className=\"flex items-start justify-between mb-2\">\n <div className=\"flex items-center gap-2\">\n {getTypeIcon(item.type)}\n <span className=\"text-sm font-medium capitalize\">{item.type}</span>\n </div>\n {downloadedItems.includes(item.id) && (\n <CheckCircle className=\"h-4 w-4 text-green-600\" />\n )}\n </div>\n <CardTitle className=\"text-lg leading-tight\">{item.title}</CardTitle>\n {item.description && (\n <CardDescription className=\"text-sm line-clamp-2\">\n {item.description}\n </CardDescription>\n )}\n </CardHeader>\n <CardContent>\n <div className=\"flex flex-wrap gap-2 mb-4\">\n {item.category && (\n <Badge className={getCategoryColor(item.category)}>\n {item.category}\n </Badge>\n )}\n {item.difficulty && (\n <Badge className={getDifficultyColor(item.difficulty)}>\n {item.difficulty}\n </Badge>\n )}\n {item.deviceType && (\n <Badge variant=\"outline\" className=\"flex items-center gap-1\">\n <Smartphone className=\"h-3 w-3\" />\n {item.deviceType}\n </Badge>\n )}\n </div>\n\n <div className=\"flex items-center justify-between mb-4\">\n <span className=\"text-sm text-muted-foreground\">\n Size: {item.size}\n </span>\n <span className=\"text-sm font-medium\">\n {item.format}\n </span>\n </div>\n\n {downloadProgress[item.id] !== undefined ? (\n <div className=\"space-y-2\">\n <Progress value={downloadProgress[item.id]} className=\"h-2\" />\n <p className=\"text-sm text-center text-muted-foreground\">\n Downloading... {downloadProgress[item.id]}%\n </p>\n </div>\n ) : (\n <Button\n onClick={() => handleDownload(item)}\n className=\"w-full\"\n disabled={downloadedItems.includes(item.id)}\n >\n {downloadedItems.includes(item.id) ? (\n <>\n <CheckCircle className=\"mr-2 h-4 w-4\" />\n Downloaded\n </>\n ) : (\n <>\n <Download className=\"mr-2 h-4 w-4\" />\n Download\n </>\n )}\n </Button>\n )}\n </CardContent>\n </Card>\n ))}\n </div>\n )}\n\n {filteredItems.length === 0 && !guidesLoading && !flowsLoading && !componentsLoading && (\n <div className=\"text-center py-12\">\n <p className=\"text-muted-foreground\">No items found matching your search criteria.</p>\n </div>\n )}\n </div>\n </main>\n </div>\n );\n}","size_bytes":20841},"client/src/pages/favorites.tsx":{"content":"import { useQuery } from \"@tanstack/react-query\";\nimport { useAuth } from \"@/hooks/useAuth\";\nimport { Button } from \"@/components/ui/button\";\nimport Header from \"@/components/layout/header\";\nimport { useToast } from \"@/hooks/use-toast\";\nimport { useEffect } from \"react\";\nimport { isUnauthorizedError } from \"@/lib/authUtils\";\nimport type { Favorite } from \"@shared/schema\";\n\nexport default function Favorites() {\n const { toast } = useToast();\n const { isAuthenticated, isLoading: authLoading } = useAuth();\n\n // Redirect to login if not authenticated\n useEffect(() => {\n if (!authLoading && !isAuthenticated) {\n toast({\n title: \"Unauthorized\",\n description: \"You are logged out. Logging in again...\",\n variant: \"destructive\",\n });\n setTimeout(() => {\n window.location.href = \"/api/login\";\n }, 500);\n return;\n }\n }, [isAuthenticated, authLoading, toast]);\n\n const { data: favorites = [], isLoading } = useQuery<Favorite[]>({\n queryKey: [\"/api/favorites\"],\n enabled: isAuthenticated,\n retry: (failureCount, error) => {\n if (isUnauthorizedError(error as Error)) {\n return false;\n }\n return failureCount < 3;\n },\n });\n\n if (authLoading || !isAuthenticated) {\n return (\n <div className=\"min-h-screen bg-gray-50 dark:bg-gray-900 amoled:bg-black flex items-center justify-center\">\n <div className=\"text-center\">\n <i className=\"material-icons text-4xl text-primary-700 mb-4 animate-spin\">refresh</i>\n <p className=\"text-gray-600 dark:text-gray-300 amoled:text-gray-300\">Loading...</p>\n </div>\n </div>\n );\n }\n\n const getFavoriteIcon = (itemType: string) => {\n switch (itemType) {\n case 'guide':\n return 'book';\n case 'video':\n return 'play_circle';\n case 'troubleshooting':\n return 'troubleshoot';\n default:\n return 'star';\n }\n };\n\n const getFavoriteTypeLabel = (itemType: string) => {\n switch (itemType) {\n case 'guide':\n return 'Repair Guide';\n case 'video':\n return 'Video Tutorial';\n case 'troubleshooting':\n return 'Troubleshooting Flow';\n default:\n return 'Favorite';\n }\n };\n\n return (\n <div className=\"min-h-screen bg-gray-50 dark:bg-gray-900 amoled:bg-black\">\n <Header />\n \n <main className=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6\">\n <div className=\"mb-8\">\n <h1 className=\"text-3xl font-bold text-gray-900 dark:text-white amoled:text-white mb-2\">\n <i className=\"material-icons text-4xl align-bottom mr-2\">favorite</i>\n My Favorites\n </h1>\n <p className=\"text-gray-600 dark:text-gray-300 amoled:text-gray-300\">\n Your saved repair guides, videos, and troubleshooting flows\n </p>\n </div>\n\n {isLoading ? (\n <div className=\"text-center py-12\">\n <i className=\"material-icons text-4xl text-primary-700 mb-4 animate-spin\">refresh</i>\n <p className=\"text-gray-600 dark:text-gray-300 amoled:text-gray-300\">Loading your favorites...</p>\n </div>\n ) : favorites.length === 0 ? (\n <div className=\"text-center py-12\">\n <i className=\"material-icons text-6xl text-gray-400 mb-4\">favorite_border</i>\n <h3 className=\"text-xl font-medium text-gray-900 dark:text-white amoled:text-white mb-2\">\n No favorites yet\n </h3>\n <p className=\"text-gray-600 dark:text-gray-300 amoled:text-gray-300 mb-6\">\n Start exploring repair guides and save your favorites for quick access\n </p>\n <Button\n onClick={() => window.location.href = '/'}\n className=\"bg-primary-700 hover:bg-primary-800 text-white\"\n >\n <i className=\"material-icons mr-2\">explore</i>\n Browse Guides\n </Button>\n </div>\n ) : (\n <div className=\"grid gap-6 md:grid-cols-2 lg:grid-cols-3\">\n {favorites.map((favorite) => (\n <div\n key={`${favorite.itemType}-${favorite.itemId}`}\n className=\"bg-white dark:bg-gray-800 amoled:bg-black rounded-xl shadow-material hover:shadow-material-lg transition-shadow\"\n >\n {favorite.itemImageUrl && (\n <img\n src={favorite.itemImageUrl}\n alt={favorite.itemTitle}\n className=\"w-full h-48 object-cover rounded-t-xl\"\n />\n )}\n \n <div className=\"p-6\">\n <div className=\"flex items-center justify-between mb-3\">\n <span className=\"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-primary-100 text-primary-800 dark:bg-primary-900 dark:text-primary-200\">\n <i className=\"material-icons text-sm mr-1\">{getFavoriteIcon(favorite.itemType)}</i>\n {getFavoriteTypeLabel(favorite.itemType)}\n </span>\n \n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-900/20\"\n >\n <i className=\"material-icons text-lg\">favorite</i>\n </Button>\n </div>\n\n <h3 className=\"font-medium text-gray-900 dark:text-white amoled:text-white mb-2\">\n {favorite.itemTitle}\n </h3>\n \n <p className=\"text-sm text-gray-500 dark:text-gray-400 amoled:text-gray-400 mb-4\">\n Added {new Date(favorite.createdAt!).toLocaleDateString()}\n </p>\n\n <Button\n className=\"w-full bg-primary-700 hover:bg-primary-800 text-white\"\n onClick={() => {\n // Navigate to the item based on type\n if (favorite.itemType === 'guide') {\n window.location.href = `/guide/${favorite.itemId}`;\n } else if (favorite.itemType === 'troubleshooting') {\n window.location.href = `/troubleshooting/${favorite.itemId}`;\n }\n }}\n >\n <i className=\"material-icons mr-2\">open_in_new</i>\n Open\n </Button>\n </div>\n </div>\n ))}\n </div>\n )}\n </main>\n </div>\n );\n}","size_bytes":6687},"client/src/pages/home.tsx":{"content":"import { useState } from \"react\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport Header from \"@/components/layout/header\";\nimport Footer from \"@/components/layout/footer\";\nimport DeviceSelector from \"@/components/repair/device-selector\";\nimport CategoryGrid from \"@/components/repair/category-grid\";\nimport GuideCard from \"@/components/repair/guide-card\";\nimport PartsIdentification from \"@/components/repair/parts-identification\";\nimport TroubleshootingFlowchart from \"@/components/repair/troubleshooting-flowchart\";\nimport SoftwareLinks from \"@/components/repair/software-links\";\nimport FloatingActionButton from \"@/components/repair/floating-action-button\";\nimport GuideDetailModal from \"@/components/repair/guide-detail-modal\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ui/select\";\nimport type { RepairGuide } from \"@shared/schema\";\n\nexport default function Home() {\n const [selectedDeviceType, setSelectedDeviceType] = useState<\"laptop\" | \"desktop\">(\"laptop\");\n const [selectedCategory, setSelectedCategory] = useState<string>(\"\");\n const [sortOption, setSortOption] = useState<string>(\"popular\");\n const [selectedGuideId, setSelectedGuideId] = useState<string | null>(null);\n\n const { data: repairGuides = [], isLoading } = useQuery<RepairGuide[]>({\n queryKey: [\"/api/repair-guides\", selectedDeviceType, selectedCategory],\n queryFn: () => {\n const params = new URLSearchParams();\n if (selectedDeviceType) params.append('deviceType', selectedDeviceType);\n if (selectedCategory) params.append('category', selectedCategory);\n return fetch(`/api/repair-guides?${params}`).then(res => res.json());\n },\n });\n\n const sortedGuides = [...repairGuides].sort((a, b) => {\n switch (sortOption) {\n case \"difficulty\":\n const difficultyOrder = { easy: 1, medium: 2, hard: 3 };\n return difficultyOrder[a.difficulty as keyof typeof difficultyOrder] - difficultyOrder[b.difficulty as keyof typeof difficultyOrder];\n case \"time\":\n return a.estimatedTime.localeCompare(b.estimatedTime);\n case \"popular\":\n default:\n return (b.viewCount || 0) - (a.viewCount || 0);\n }\n });\n\n const filteredGuides = selectedCategory \n ? sortedGuides.filter(guide => guide.category === selectedCategory)\n : sortedGuides;\n\n return (\n <div className=\"min-h-screen bg-gray-50 dark:bg-gray-900 amoled:bg-black\">\n <Header />\n \n <main className=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6\">\n {/* Device Type Selection */}\n <section className=\"mb-8\">\n <DeviceSelector \n selectedType={selectedDeviceType} \n onTypeChange={setSelectedDeviceType} \n />\n </section>\n\n {/* Category Grid */}\n <section className=\"mb-8\">\n <CategoryGrid \n selectedCategory={selectedCategory}\n onCategoryChange={setSelectedCategory}\n deviceType={selectedDeviceType}\n />\n </section>\n\n {/* Repair Guides List */}\n <section className=\"mb-8\">\n <div className=\"flex items-center justify-between mb-4\">\n <h2 className=\"text-lg font-medium text-gray-900 dark:text-white\">Popular Repair Guides</h2>\n <div className=\"flex items-center space-x-4\">\n <Select value={sortOption} onValueChange={setSortOption}>\n <SelectTrigger className=\"w-48\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"popular\">Most Popular</SelectItem>\n <SelectItem value=\"difficulty\">Difficulty: Easy to Hard</SelectItem>\n <SelectItem value=\"time\">Estimated Time</SelectItem>\n </SelectContent>\n </Select>\n </div>\n </div>\n\n {isLoading ? (\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-6\">\n {[...Array(4)].map((_, i) => (\n <div key={i} className=\"bg-white dark:bg-gray-800 rounded-xl shadow-material animate-pulse\">\n <div className=\"h-48 bg-gray-200 rounded-t-xl\"></div>\n <div className=\"p-6 space-y-3\">\n <div className=\"h-4 bg-gray-200 rounded w-3/4\"></div>\n <div className=\"h-3 bg-gray-200 rounded w-full\"></div>\n <div className=\"h-3 bg-gray-200 rounded w-5/6\"></div>\n </div>\n </div>\n ))}\n </div>\n ) : (\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-6\">\n {filteredGuides.map(guide => (\n <GuideCard \n key={guide.id} \n guide={guide} \n onViewGuide={() => setSelectedGuideId(guide.id)}\n />\n ))}\n </div>\n )}\n\n {!isLoading && filteredGuides.length === 0 && (\n <div className=\"text-center py-12\">\n <div className=\"text-gray-400 mb-4\">\n <i className=\"material-icons text-6xl\">search_off</i>\n </div>\n <h3 className=\"text-lg font-medium text-gray-900 dark:text-white mb-2\">No guides found</h3>\n <p className=\"text-gray-600 dark:text-gray-300\">Try adjusting your filters or selecting a different device type.</p>\n </div>\n )}\n </section>\n\n {/* Troubleshooting Flowchart */}\n <section className=\"mb-8\">\n <TroubleshootingFlowchart />\n </section>\n\n {/* Parts Identification */}\n <section className=\"mb-8\">\n <PartsIdentification deviceType={selectedDeviceType} />\n </section>\n\n {/* Software Tools */}\n <section className=\"mb-8\" data-section=\"software-tools\">\n <SoftwareLinks deviceType={selectedDeviceType} />\n </section>\n </main>\n\n <FloatingActionButton />\n \n {selectedGuideId && (\n <GuideDetailModal \n guideId={selectedGuideId}\n onClose={() => setSelectedGuideId(null)}\n />\n )}\n \n <Footer />\n </div>\n );\n}\n","size_bytes":6168},"client/src/pages/landing.tsx":{"content":"import { Button } from \"@/components/ui/button\";\nimport Footer from \"@/components/layout/footer\";\n\nexport default function Landing() {\n return (\n <div className=\"min-h-screen bg-gradient-to-br from-primary-50 to-primary-100 dark:from-gray-900 dark:to-gray-800 amoled:from-black amoled:to-black flex flex-col items-center justify-center\">\n <div className=\"w-full max-w-4xl mx-auto text-center px-4 sm:px-6 lg:px-8 flex flex-col items-center justify-center\">\n <div className=\"mb-8 flex flex-col items-center\">\n <i className=\"material-icons text-6xl text-primary-700 mb-4\">build</i>\n <h1 className=\"text-4xl md:text-6xl font-bold text-gray-900 dark:text-white amoled:text-white mb-4 text-center\">\n JCR Guide Pro\n </h1>\n <p className=\"text-xl md:text-2xl text-gray-600 dark:text-gray-300 amoled:text-gray-300 mb-8 text-center\">\n Your complete laptop and PC repair companion\n </p>\n </div>\n\n <div className=\"grid md:grid-cols-3 gap-8 mb-12 w-full max-w-5xl mx-auto\">\n <div className=\"bg-white dark:bg-gray-800 amoled:bg-black rounded-xl p-6 shadow-material\">\n <i className=\"material-icons text-4xl text-primary-700 mb-4\">book</i>\n <h3 className=\"text-lg font-semibold text-gray-900 dark:text-white amoled:text-white mb-2\">\n Step-by-Step Guides\n </h3>\n <p className=\"text-gray-600 dark:text-gray-300 amoled:text-gray-300\">\n Detailed repair instructions with visual aids and safety warnings\n </p>\n </div>\n\n <div className=\"bg-white dark:bg-gray-800 amoled:bg-black rounded-xl p-6 shadow-material\">\n <i className=\"material-icons text-4xl text-primary-700 mb-4\">troubleshoot</i>\n <h3 className=\"text-lg font-semibold text-gray-900 dark:text-white amoled:text-white mb-2\">\n Troubleshooting Tools\n </h3>\n <p className=\"text-gray-600 dark:text-gray-300 amoled:text-gray-300\">\n Interactive flowcharts to diagnose and solve common problems\n </p>\n </div>\n\n <div className=\"bg-white dark:bg-gray-800 amoled:bg-black rounded-xl p-6 shadow-material\">\n <i className=\"material-icons text-4xl text-primary-700 mb-4\">favorite</i>\n <h3 className=\"text-lg font-semibold text-gray-900 dark:text-white amoled:text-white mb-2\">\n Save Favorites\n </h3>\n <p className=\"text-gray-600 dark:text-gray-300 amoled:text-gray-300\">\n Bookmark your most-used guides and access them instantly\n </p>\n </div>\n </div>\n\n <div className=\"flex flex-col items-center space-y-4\">\n <Button\n onClick={() => window.location.href = '/api/login'}\n size=\"lg\"\n className=\"bg-primary-700 hover:bg-primary-800 text-white px-8 py-3 text-lg\"\n >\n <i className=\"material-icons mr-2\">login</i>\n Sign In to Get Started\n </Button>\n \n <p className=\"text-sm text-gray-500 dark:text-gray-400 amoled:text-gray-400 text-center\">\n Sign in securely to access your personalized repair guides and save your favorites\n </p>\n </div>\n\n <div className=\"mt-16 grid md:grid-cols-4 gap-6 text-sm text-gray-500 dark:text-gray-400 amoled:text-gray-400 w-full max-w-2xl mx-auto text-center\">\n <div>\n <strong className=\"text-primary-700\">20+</strong><br />\n Repair Guides\n </div>\n <div>\n <strong className=\"text-primary-700\">5+</strong><br />\n Device Types\n </div>\n <div>\n <strong className=\"text-primary-700\">10+</strong><br />\n Software Tools\n </div>\n <div>\n <strong className=\"text-primary-700\">Safe</strong><br />\n & Secure\n </div>\n </div>\n </div>\n \n <Footer />\n </div>\n );\n}","size_bytes":4022},"client/src/pages/not-found.tsx":{"content":"import { Card, CardContent } from \"@/components/ui/card\";\nimport { AlertCircle } from \"lucide-react\";\n\nexport default function NotFound() {\n return (\n <div className=\"min-h-screen w-full flex items-center justify-center bg-gray-50\">\n <Card className=\"w-full max-w-md mx-4\">\n <CardContent className=\"pt-6\">\n <div className=\"flex mb-4 gap-2\">\n <AlertCircle className=\"h-8 w-8 text-red-500\" />\n <h1 className=\"text-2xl font-bold text-gray-900\">404 Page Not Found</h1>\n </div>\n\n <p className=\"mt-4 text-sm text-gray-600\">\n Did you forget to add the page to the router?\n </p>\n </CardContent>\n </Card>\n </div>\n );\n}\n","size_bytes":711},"client/src/pages/offline-settings.tsx":{"content":"import React from \"react\";\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { Button } from \"@/components/ui/button\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Progress } from \"@/components/ui/progress\";\nimport { useToast } from \"@/hooks/use-toast\";\nimport { useOfflineMode } from \"@/hooks/useOfflineMode\";\nimport { formatStorageSize } from \"@/lib/offline-storage\";\nimport { \n Download, \n Upload, \n Wifi, \n WifiOff, \n HardDrive, \n Trash2, \n RefreshCw,\n CheckCircle,\n AlertCircle,\n Clock\n} from \"lucide-react\";\n\nexport default function OfflineSettings() {\n const { toast } = useToast();\n const {\n isOfflineSupported,\n isOfflineModeEnabled,\n isCurrentlyOnline,\n lastSyncTime,\n storageInfo,\n enableOfflineMode,\n disableOfflineMode,\n forceSync,\n getOfflineData,\n } = useOfflineMode();\n\n const handleEnableOffline = async () => {\n const success = await enableOfflineMode();\n if (success) {\n toast({\n title: \"Offline Mode Enabled\",\n description: \"Repair guides have been cached for offline use.\",\n });\n } else {\n toast({\n title: \"Failed to Enable Offline Mode\",\n description: \"Please check your connection and try again.\",\n variant: \"destructive\",\n });\n }\n };\n\n const handleDisableOffline = () => {\n disableOfflineMode();\n toast({\n title: \"Offline Mode Disabled\",\n description: \"Offline data will no longer be used.\",\n });\n };\n\n const handleForceSync = async () => {\n if (!isCurrentlyOnline) {\n toast({\n title: \"No Internet Connection\",\n description: \"Connect to the internet to sync data.\",\n variant: \"destructive\",\n });\n return;\n }\n\n const success = await forceSync();\n if (success) {\n toast({\n title: \"Sync Complete\",\n description: \"All data has been synchronized.\",\n });\n } else {\n toast({\n title: \"Sync Failed\",\n description: \"Please check your connection and try again.\",\n variant: \"destructive\",\n });\n }\n };\n\n const offlineData = getOfflineData();\n\n if (!isOfflineSupported) {\n return (\n <div className=\"container mx-auto py-8 px-4\">\n <div className=\"max-w-4xl mx-auto\">\n <h1 className=\"text-3xl font-bold mb-6\">Offline Mode</h1>\n \n <Card>\n <CardContent className=\"p-8 text-center\">\n <AlertCircle className=\"h-16 w-16 mx-auto mb-4 text-gray-400\" />\n <h2 className=\"text-xl font-semibold mb-2\">Offline Mode Not Supported</h2>\n <p className=\"text-muted-foreground\">\n Your browser doesn't support the features required for offline mode.\n Please use a modern browser with localStorage support.\n </p>\n </CardContent>\n </Card>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"container mx-auto py-8 px-4\">\n <div className=\"max-w-4xl mx-auto\">\n <div className=\"mb-6\">\n <h1 className=\"text-3xl font-bold mb-2\">Offline Mode Settings</h1>\n <p className=\"text-muted-foreground\">\n Configure offline access to repair guides for when you don't have internet connectivity.\n </p>\n </div>\n\n {/* Connection Status */}\n <Card className=\"mb-6\">\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n {isCurrentlyOnline ? (\n <>\n <Wifi className=\"h-5 w-5 text-green-600\" />\n Online\n </>\n ) : (\n <>\n <WifiOff className=\"h-5 w-5 text-red-600\" />\n Offline\n </>\n )}\n </CardTitle>\n <CardDescription>\n {isCurrentlyOnline \n ? \"You're connected to the internet and can access all features.\"\n : \"You're offline. Only cached content is available.\"\n }\n </CardDescription>\n </CardHeader>\n <CardContent>\n <div className=\"flex items-center gap-4\">\n <Badge variant={isCurrentlyOnline ? \"default\" : \"destructive\"}>\n {isCurrentlyOnline ? \"Connected\" : \"Disconnected\"}\n </Badge>\n {lastSyncTime && (\n <div className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n <Clock className=\"h-4 w-4\" />\n Last sync: {lastSyncTime.toLocaleString()}\n </div>\n )}\n </div>\n </CardContent>\n </Card>\n\n {/* Offline Mode Control */}\n <Card className=\"mb-6\">\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <Download className=\"h-5 w-5\" />\n Offline Mode\n </CardTitle>\n <CardDescription>\n Enable offline mode to cache repair guides for use without internet connectivity.\n </CardDescription>\n </CardHeader>\n <CardContent>\n <div className=\"flex items-center justify-between mb-4\">\n <div>\n <div className=\"flex items-center gap-2 mb-1\">\n <span className=\"font-medium\">Status:</span>\n <Badge variant={isOfflineModeEnabled ? \"default\" : \"secondary\"}>\n {isOfflineModeEnabled ? \"Enabled\" : \"Disabled\"}\n </Badge>\n </div>\n <p className=\"text-sm text-muted-foreground\">\n {isOfflineModeEnabled \n ? \"Guides are cached and available offline\"\n : \"Offline access is not enabled\"\n }\n </p>\n </div>\n <div className=\"flex gap-2\">\n {isOfflineModeEnabled ? (\n <Button onClick={handleDisableOffline} variant=\"outline\">\n Disable Offline Mode\n </Button>\n ) : (\n <Button \n onClick={handleEnableOffline}\n disabled={!isCurrentlyOnline}\n >\n <Download className=\"h-4 w-4 mr-2\" />\n Enable Offline Mode\n </Button>\n )}\n \n {isOfflineModeEnabled && (\n <Button \n onClick={handleForceSync}\n disabled={!isCurrentlyOnline}\n variant=\"outline\"\n >\n <RefreshCw className=\"h-4 w-4 mr-2\" />\n Sync Now\n </Button>\n )}\n </div>\n </div>\n\n {!isCurrentlyOnline && !isOfflineModeEnabled && (\n <div className=\"p-4 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg\">\n <div className=\"flex items-center gap-2 text-amber-800 dark:text-amber-200\">\n <AlertCircle className=\"h-4 w-4\" />\n <span className=\"text-sm font-medium\">\n You're offline and don't have cached content. Connect to the internet to enable offline mode.\n </span>\n </div>\n </div>\n )}\n </CardContent>\n </Card>\n\n {/* Storage Information */}\n <Card className=\"mb-6\">\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <HardDrive className=\"h-5 w-5\" />\n Storage Usage\n </CardTitle>\n <CardDescription>\n Local storage usage for offline content.\n </CardDescription>\n </CardHeader>\n <CardContent>\n <div className=\"space-y-4\">\n <div>\n <div className=\"flex justify-between items-center mb-2\">\n <span className=\"text-sm font-medium\">Storage Used</span>\n <span className=\"text-sm text-muted-foreground\">\n {formatStorageSize(storageInfo.used)} / {formatStorageSize(storageInfo.available)}\n </span>\n </div>\n <Progress value={storageInfo.percentage} className=\"h-2\" />\n <p className=\"text-xs text-muted-foreground mt-1\">\n {storageInfo.percentage.toFixed(1)}% of available storage used\n </p>\n </div>\n\n {storageInfo.percentage > 80 && (\n <div className=\"p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg\">\n <div className=\"flex items-center gap-2 text-amber-800 dark:text-amber-200\">\n <AlertCircle className=\"h-4 w-4\" />\n <span className=\"text-sm\">\n Storage is getting full. Consider clearing offline data to free up space.\n </span>\n </div>\n </div>\n )}\n </div>\n </CardContent>\n </Card>\n\n {/* Cached Content Summary */}\n {isOfflineModeEnabled && (\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <CheckCircle className=\"h-5 w-5\" />\n Cached Content\n </CardTitle>\n <CardDescription>\n Content available for offline use.\n </CardDescription>\n </CardHeader>\n <CardContent>\n <div className=\"grid grid-cols-1 md:grid-cols-3 gap-4\">\n <div className=\"text-center p-4 bg-gray-50 dark:bg-gray-800 rounded-lg\">\n <div className=\"text-2xl font-bold text-blue-600\">{offlineData.guides.length}</div>\n <div className=\"text-sm text-muted-foreground\">Repair Guides</div>\n </div>\n \n <div className=\"text-center p-4 bg-gray-50 dark:bg-gray-800 rounded-lg\">\n <div className=\"text-2xl font-bold text-green-600\">{offlineData.flows.length}</div>\n <div className=\"text-sm text-muted-foreground\">Troubleshooting Flows</div>\n </div>\n \n <div className=\"text-center p-4 bg-gray-50 dark:bg-gray-800 rounded-lg\">\n <div className=\"text-2xl font-bold text-purple-600\">{offlineData.components.length}</div>\n <div className=\"text-sm text-muted-foreground\">Device Components</div>\n </div>\n </div>\n\n {offlineData.userGuides.length > 0 && (\n <div className=\"mt-4 p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg\">\n <div className=\"flex items-center gap-2 text-blue-800 dark:text-blue-200 mb-2\">\n <Upload className=\"h-4 w-4\" />\n <span className=\"text-sm font-medium\">\n {offlineData.userGuides.length} user guide(s) pending sync\n </span>\n </div>\n <p className=\"text-xs text-blue-700 dark:text-blue-300\">\n These guides will be uploaded to the server when you're back online.\n </p>\n </div>\n )}\n </CardContent>\n </Card>\n )}\n </div>\n </div>\n );\n}","size_bytes":11506},"client/src/pages/repair-guide.tsx":{"content":"import { useParams } from \"wouter\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport Header from \"@/components/layout/header\";\nimport GuideDetailModal from \"@/components/repair/guide-detail-modal\";\nimport type { RepairGuide } from \"@shared/schema\";\n\nexport default function RepairGuide() {\n const { id } = useParams<{ id: string }>();\n\n const { data: guide, isLoading, error } = useQuery<RepairGuide>({\n queryKey: [\"/api/repair-guides\", id],\n enabled: !!id,\n });\n\n if (isLoading) {\n return (\n <div className=\"min-h-screen bg-gray-50\">\n <Header />\n <div className=\"max-w-4xl mx-auto px-4 py-8\">\n <div className=\"bg-white rounded-xl shadow-material p-6 animate-pulse\">\n <div className=\"h-8 bg-gray-200 rounded w-3/4 mb-4\"></div>\n <div className=\"h-4 bg-gray-200 rounded w-full mb-2\"></div>\n <div className=\"h-4 bg-gray-200 rounded w-5/6 mb-6\"></div>\n <div className=\"space-y-4\">\n {[...Array(3)].map((_, i) => (\n <div key={i} className=\"h-24 bg-gray-200 rounded\"></div>\n ))}\n </div>\n </div>\n </div>\n </div>\n );\n }\n\n if (error || !guide) {\n return (\n <div className=\"min-h-screen bg-gray-50\">\n <Header />\n <div className=\"max-w-4xl mx-auto px-4 py-8\">\n <div className=\"bg-white rounded-xl shadow-material p-6 text-center\">\n <div className=\"text-gray-400 mb-4\">\n <i className=\"material-icons text-6xl\">error_outline</i>\n </div>\n <h2 className=\"text-xl font-medium text-gray-900 mb-2\">Guide Not Found</h2>\n <p className=\"text-gray-600\">The repair guide you're looking for doesn't exist or has been removed.</p>\n </div>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"min-h-screen bg-gray-50\">\n <Header />\n <GuideDetailModal \n guideId={guide.id}\n onClose={() => window.history.back()}\n isPage={true}\n />\n </div>\n );\n}\n","size_bytes":2042},"client/src/pages/software-tools.tsx":{"content":"import { useState } from \"react\";\nimport Header from \"@/components/layout/header\";\nimport Footer from \"@/components/layout/footer\";\nimport SoftwareLinks from \"@/components/repair/software-links\";\n\nexport default function SoftwareTools() {\n const [selectedCategory, setSelectedCategory] = useState<string>(\"all\");\n\n const categories = [\n { id: \"all\", name: \"All Tools\", icon: \"apps\" },\n { id: \"diagnostics\", name: \"Hardware Diagnostics\", icon: \"monitor_heart\" },\n { id: \"recovery\", name: \"Data Recovery\", icon: \"restore\" },\n { id: \"security\", name: \"Security & Malware\", icon: \"security\" },\n { id: \"system\", name: \"System Utilities\", icon: \"settings\" },\n { id: \"drivers\", name: \"Driver Tools\", icon: \"update\" },\n ];\n\n const allSoftware = [\n // Hardware Diagnostics\n {\n name: \"HWiNFO64\",\n description: \"Comprehensive hardware analysis, monitoring, and reporting tool for system diagnostics\",\n url: \"https://www.hwinfo.com/\",\n category: \"diagnostics\",\n icon: \"monitor_heart\",\n free: true,\n features: [\"Real-time monitoring\", \"Detailed hardware info\", \"Sensor data logging\"],\n },\n {\n name: \"CrystalDiskInfo\",\n description: \"Monitor hard drive health with S.M.A.R.T. data analysis and temperature tracking\",\n url: \"https://crystalmark.info/en/software/crystaldiskinfo/\",\n category: \"diagnostics\",\n icon: \"storage\",\n free: true,\n features: [\"Health status\", \"Temperature monitoring\", \"S.M.A.R.T. analysis\"],\n },\n {\n name: \"MemTest86\",\n description: \"Industry-standard memory testing tool for detecting RAM defects and stability issues\",\n url: \"https://www.memtest86.com/\",\n category: \"diagnostics\",\n icon: \"memory\",\n free: true,\n features: [\"Boot from USB\", \"Comprehensive testing\", \"Error detection\"],\n },\n {\n name: \"GPU-Z\",\n description: \"Graphics card information utility with real-time monitoring and validation\",\n url: \"https://www.techpowerup.com/gpuz/\",\n category: \"diagnostics\",\n icon: \"videogame_asset\",\n free: true,\n features: [\"GPU specifications\", \"Real-time monitoring\", \"BIOS backup\"],\n },\n {\n name: \"Prime95\",\n description: \"CPU stress testing tool for stability testing and thermal validation\",\n url: \"https://www.mersenne.org/download/\",\n category: \"diagnostics\",\n icon: \"speed\",\n free: true,\n features: [\"CPU stress test\", \"Temperature monitoring\", \"Stability verification\"],\n },\n \n // Data Recovery & Backup\n {\n name: \"Recuva\",\n description: \"File recovery tool for deleted files from hard drives, memory cards, and USB drives\",\n url: \"https://www.ccleaner.com/recuva\",\n category: \"recovery\",\n icon: \"restore\",\n free: true,\n features: [\"Deleted file recovery\", \"Deep scan mode\", \"Secure deletion\"],\n },\n {\n name: \"PhotoRec\",\n description: \"Free data recovery software for lost files including photos, documents, and archives\",\n url: \"https://www.cgsecurity.org/wiki/PhotoRec\",\n category: \"recovery\",\n icon: \"photo_library\",\n free: true,\n features: [\"File carving\", \"Multiple formats\", \"Cross-platform\"],\n },\n {\n name: \"Clonezilla\",\n description: \"Free disk imaging and cloning solution for backup and system deployment\",\n url: \"https://clonezilla.org/\",\n category: \"recovery\",\n icon: \"content_copy\",\n free: true,\n features: [\"Disk cloning\", \"Image creation\", \"Network deployment\"],\n },\n {\n name: \"Macrium Reflect\",\n description: \"Professional disk imaging software with incremental backup and scheduling\",\n url: \"https://www.macrium.com/reflectfree\",\n category: \"recovery\",\n icon: \"backup\",\n free: true,\n features: [\"Disk imaging\", \"Incremental backup\", \"Boot rescue media\"],\n },\n\n // Security & Malware\n {\n name: \"Malwarebytes\",\n description: \"Advanced anti-malware protection with real-time threat detection\",\n url: \"https://www.malwarebytes.com/\",\n category: \"security\",\n icon: \"security\",\n free: false,\n features: [\"Real-time protection\", \"Malware removal\", \"Web protection\"],\n },\n {\n name: \"ESET Online Scanner\",\n description: \"Free online antivirus scanner for detecting and removing malware\",\n url: \"https://www.eset.com/us/home/online-scanner/\",\n category: \"security\",\n icon: \"verified_user\",\n free: true,\n features: [\"On-demand scanning\", \"No installation\", \"Cloud-based detection\"],\n },\n {\n name: \"RootkitRevealer\",\n description: \"Microsoft tool for detecting rootkits and other malicious software\",\n url: \"https://docs.microsoft.com/en-us/sysinternals/downloads/rootkitrevealer\",\n category: \"security\",\n icon: \"search\",\n free: true,\n features: [\"Rootkit detection\", \"System comparison\", \"Advanced scanning\"],\n },\n\n // System Utilities\n {\n name: \"Sysinternals Suite\",\n description: \"Microsoft's collection of advanced system utilities and troubleshooting tools\",\n url: \"https://docs.microsoft.com/en-us/sysinternals/\",\n category: \"system\",\n icon: \"settings\",\n free: true,\n features: [\"Process monitoring\", \"System analysis\", \"Registry tools\"],\n },\n {\n name: \"Nirsoft Utilities\",\n description: \"Collection of small utilities for Windows system analysis and troubleshooting\",\n url: \"https://www.nirsoft.net/\",\n category: \"system\",\n icon: \"build\",\n free: true,\n features: [\"Password recovery\", \"Network monitoring\", \"System information\"],\n },\n {\n name: \"CCleaner\",\n description: \"System cleaning and optimization tool for removing junk files and registry errors\",\n url: \"https://www.ccleaner.com/\",\n category: \"system\",\n icon: \"cleaning_services\",\n free: true,\n features: [\"Junk file cleanup\", \"Registry cleaning\", \"Startup management\"],\n },\n {\n name: \"TreeSize\",\n description: \"Disk space analyzer to visualize and manage storage usage\",\n url: \"https://www.jam-software.com/treesize/\",\n category: \"system\",\n icon: \"folder\",\n free: true,\n features: [\"Disk space analysis\", \"Visual representation\", \"File management\"],\n },\n\n // Driver Tools\n {\n name: \"DDU (Display Driver Uninstaller)\",\n description: \"Complete graphics driver removal tool for clean driver installations\",\n url: \"https://www.guru3d.com/files-details/display-driver-uninstaller-download.html\",\n category: \"drivers\",\n icon: \"cleaning_services\",\n free: true,\n features: [\"Clean driver removal\", \"Safe mode operation\", \"Multiple GPU support\"],\n },\n {\n name: \"DriverBooster\",\n description: \"Automatic driver update tool for keeping system drivers current\",\n url: \"https://www.iobit.com/en/driver-booster.php\",\n category: \"drivers\",\n icon: \"update\",\n free: true,\n features: [\"Automatic updates\", \"Driver backup\", \"Game optimization\"],\n },\n {\n name: \"Snappy Driver Installer\",\n description: \"Offline driver installer with comprehensive driver database\",\n url: \"https://sdi-tool.org/\",\n category: \"drivers\",\n icon: \"offline_bolt\",\n free: true,\n features: [\"Offline installation\", \"No internet required\", \"Driver packs\"],\n },\n\n // Bootable Media & Recovery\n {\n name: \"Rufus\",\n description: \"Create bootable USB drives for OS installation and system recovery\",\n url: \"https://rufus.ie/\",\n category: \"recovery\",\n icon: \"usb\",\n free: true,\n features: [\"Bootable USB creation\", \"ISO support\", \"UEFI/Legacy support\"],\n },\n {\n name: \"Hiren's BootCD PE\",\n description: \"Bootable rescue disk with comprehensive diagnostic and repair tools\",\n url: \"https://www.hirensbootcd.org/\",\n category: \"recovery\",\n icon: \"disc_full\",\n free: true,\n features: [\"Boot from USB/DVD\", \"Multiple tools included\", \"Windows PE based\"],\n },\n ];\n\n const filteredSoftware = selectedCategory === \"all\" \n ? allSoftware \n : allSoftware.filter(software => software.category === selectedCategory);\n\n const handleDownload = (url: string, name: string) => {\n window.open(url, '_blank', 'noopener,noreferrer');\n };\n\n return (\n <div className=\"min-h-screen bg-gray-50 dark:bg-gray-900 amoled:bg-black\">\n <Header />\n \n <main className=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6\">\n <div className=\"mb-8\">\n <h1 className=\"text-2xl font-medium text-gray-900 dark:text-white mb-2\">Software Tools</h1>\n <p className=\"text-gray-600 dark:text-gray-300\">Essential diagnostic, repair, and recovery software for computer troubleshooting</p>\n </div>\n\n {/* Category Filter */}\n <div className=\"mb-8\">\n <div className=\"flex flex-wrap gap-3\">\n {categories.map((category) => (\n <button\n key={category.id}\n onClick={() => setSelectedCategory(category.id)}\n className={`flex items-center space-x-2 px-4 py-2 rounded-lg transition-colors ${\n selectedCategory === category.id\n ? \"bg-primary-700 text-white\"\n : \"bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700\"\n }`}\n >\n <i className=\"material-icons text-sm\">{category.icon}</i>\n <span className=\"text-sm font-medium\">{category.name}</span>\n </button>\n ))}\n </div>\n </div>\n\n {/* Software Grid */}\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6\">\n {filteredSoftware.map((software, index) => (\n <div\n key={index}\n className=\"bg-white dark:bg-gray-800 rounded-xl shadow-material hover:shadow-material-lg transition-shadow p-6\"\n >\n <div className=\"flex items-start justify-between mb-4\">\n <div className=\"flex items-center space-x-3\">\n <i className={`material-icons text-primary-700 text-2xl`}>{software.icon}</i>\n <div>\n <h3 className=\"font-medium text-gray-900 dark:text-white\">{software.name}</h3>\n <div className=\"flex items-center space-x-2 mt-1\">\n <span className=\"text-xs text-gray-500 dark:text-gray-400 capitalize\">{software.category}</span>\n {software.free ? (\n <span className=\"flex items-center text-xs text-success-600\">\n <i className=\"material-icons text-xs mr-1\">check_circle</i>\n Free\n </span>\n ) : (\n <span className=\"flex items-center text-xs text-warning-700\">\n <i className=\"material-icons text-xs mr-1\">paid</i>\n Paid\n </span>\n )}\n </div>\n </div>\n </div>\n </div>\n \n <p className=\"text-sm text-gray-600 dark:text-gray-300 mb-4\">\n {software.description}\n </p>\n\n {software.features && (\n <div className=\"mb-4\">\n <h4 className=\"text-xs font-medium text-gray-900 dark:text-white mb-2\">Key Features:</h4>\n <ul className=\"text-xs text-gray-600 dark:text-gray-300 space-y-1\">\n {software.features.map((feature, featureIndex) => (\n <li key={featureIndex} className=\"flex items-center\">\n <i className=\"material-icons text-xs text-primary-700 mr-2\">check</i>\n {feature}\n </li>\n ))}\n </ul>\n </div>\n )}\n \n <button\n onClick={() => handleDownload(software.url, software.name)}\n className=\"w-full px-4 py-2 bg-primary-700 text-white text-sm rounded-lg hover:bg-primary-900 transition-colors flex items-center justify-center space-x-2\"\n >\n <i className=\"material-icons text-sm\">download</i>\n <span>Download / Visit</span>\n </button>\n </div>\n ))}\n </div>\n\n {/* Safety Information */}\n <div className=\"mt-12 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-xl p-6\">\n <div className=\"flex items-start space-x-4\">\n <i className=\"material-icons text-primary-700 text-2xl mt-1\">info</i>\n <div>\n <h3 className=\"font-medium text-primary-900 dark:text-primary-300 mb-3\">Important Safety Guidelines</h3>\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4 text-sm text-primary-800 dark:text-primary-200\">\n <div>\n <h4 className=\"font-medium mb-2\">Before Downloading:</h4>\n <ul className=\"space-y-1\">\n <li>• Always download from official sources</li>\n <li>• Check digital signatures when available</li>\n <li>• Read user reviews and ratings</li>\n <li>• Scan downloads with antivirus software</li>\n </ul>\n </div>\n <div>\n <h4 className=\"font-medium mb-2\">Before Running Tools:</h4>\n <ul className=\"space-y-1\">\n <li>• Create full system backup</li>\n <li>• Close unnecessary programs</li>\n <li>• Run as administrator when required</li>\n <li>• Have recovery plan ready</li>\n </ul>\n </div>\n </div>\n </div>\n </div>\n </div>\n {/* Contact Support Section */}\n <section className=\"mt-8 bg-white dark:bg-gray-800 amoled:bg-black rounded-xl shadow-material p-6\">\n <div className=\"text-center\">\n <h2 className=\"text-lg font-medium text-gray-900 dark:text-white mb-3 flex items-center justify-center\">\n <i className=\"material-icons text-primary-700 mr-2\">support_agent</i>\n Need Help Finding the Right Tool?\n </h2>\n <p className=\"text-gray-600 dark:text-gray-400 mb-4\">\n Can't find the software you need or need assistance with tool selection? Contact our app creator for personalized recommendations.\n </p>\n <a \n href=\"mailto:JCRguideproofficial@gmail.com\"\n className=\"inline-flex items-center bg-primary-700 hover:bg-primary-800 text-white px-6 py-2 rounded-lg font-medium transition-colors\"\n >\n <i className=\"material-icons text-sm mr-2\">email</i>\n Contact Support: JCRguideproofficial@gmail.com\n </a>\n </div>\n </section>\n </main>\n \n <Footer />\n </div>\n );\n}","size_bytes":15229},"client/src/pages/troubleshooting.tsx":{"content":"import { useParams } from \"wouter\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport Header from \"@/components/layout/header\";\nimport TroubleshootingFlowchart from \"@/components/repair/troubleshooting-flowchart\";\nimport type { TroubleshootingFlow } from \"@shared/schema\";\n\nexport default function Troubleshooting() {\n const { type } = useParams<{ type?: string }>();\n\n const { data: flows = [], isLoading } = useQuery<TroubleshootingFlow[]>({\n queryKey: [\"/api/troubleshooting-flows\"],\n });\n\n const selectedFlow = flows.find(flow => flow.type === type);\n\n return (\n <div className=\"min-h-screen bg-gray-50\">\n <Header />\n \n <main className=\"max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6\">\n <div className=\"mb-6\">\n <h1 className=\"text-2xl font-medium text-gray-900 mb-2\">Troubleshooting Assistant</h1>\n <p className=\"text-gray-600\">Follow the interactive guide to diagnose and fix your device issues.</p>\n </div>\n\n {isLoading ? (\n <div className=\"bg-white rounded-xl shadow-material p-6 animate-pulse\">\n <div className=\"h-6 bg-gray-200 rounded w-1/2 mb-4\"></div>\n <div className=\"space-y-3\">\n {[...Array(3)].map((_, i) => (\n <div key={i} className=\"h-4 bg-gray-200 rounded w-full\"></div>\n ))}\n </div>\n </div>\n ) : (\n <>\n <TroubleshootingFlowchart selectedFlow={selectedFlow} />\n \n {/* Help & Contact Section */}\n <div className=\"mt-8 bg-white dark:bg-gray-800 amoled:bg-black rounded-xl shadow-material p-6\">\n <h2 className=\"text-lg font-medium text-gray-900 dark:text-white mb-4 flex items-center\">\n <i className=\"material-icons text-primary-700 mr-2\">help_center</i>\n Need Additional Help?\n </h2>\n \n <div className=\"grid md:grid-cols-2 gap-6\">\n <div className=\"space-y-4\">\n <h3 className=\"font-medium text-gray-900 dark:text-white\">Quick Support Options</h3>\n <div className=\"space-y-3 text-sm\">\n <div className=\"flex items-center\">\n <i className=\"material-icons text-blue-600 text-lg mr-3\">live_help</i>\n <span>Browse our comprehensive repair guides</span>\n </div>\n <div className=\"flex items-center\">\n <i className=\"material-icons text-green-600 text-lg mr-3\">build</i>\n <span>Use our software diagnostic tools</span>\n </div>\n <div className=\"flex items-center\">\n <i className=\"material-icons text-purple-600 text-lg mr-3\">search</i>\n <span>Search for device-specific solutions</span>\n </div>\n </div>\n </div>\n \n <div className=\"space-y-4\">\n <h3 className=\"font-medium text-gray-900 dark:text-white\">Contact Support</h3>\n <div className=\"bg-gray-50 dark:bg-gray-700 amoled:bg-gray-900 rounded-lg p-4\">\n <div className=\"flex items-center mb-2\">\n <i className=\"material-icons text-primary-700 text-lg mr-2\">email</i>\n <span className=\"font-medium text-gray-900 dark:text-white\">Email Support</span>\n </div>\n <p className=\"text-sm text-gray-600 dark:text-gray-400 mb-3\">\n Can't find what you're looking for? Contact our app creator directly:\n </p>\n <a \n href=\"mailto:JCRguideproofficial@gmail.com\"\n className=\"inline-flex items-center text-primary-700 hover:text-primary-800 dark:text-primary-400 dark:hover:text-primary-300 font-medium text-sm\"\n >\n JCRguideproofficial@gmail.com\n <i className=\"material-icons text-sm ml-1\">open_in_new</i>\n </a>\n </div>\n </div>\n </div>\n </div>\n </>\n )}\n </main>\n </div>\n );\n}\n","size_bytes":4259},"client/src/utils/performance.ts":{"content":"// Performance optimization utilities\n\n// Preload critical routes and data\nexport function preloadCriticalRoutes() {\n // Preload the most common pages users visit\n const criticalImports = [\n () => import(\"@/pages/device-finder\"),\n () => import(\"@/pages/troubleshooting\"),\n () => import(\"@/pages/repair-guide\"),\n ];\n\n // Use requestIdleCallback to preload during idle time\n if ('requestIdleCallback' in window) {\n criticalImports.forEach((importFn, index) => {\n window.requestIdleCallback(() => {\n importFn().catch(() => {}); // Silent fail for preloading\n }, { timeout: 1000 + (index * 500) });\n });\n } else {\n // Fallback for browsers without requestIdleCallback\n setTimeout(() => {\n criticalImports.forEach((importFn) => {\n importFn().catch(() => {});\n });\n }, 2000);\n }\n}\n\n// Optimize images with lazy loading and proper sizing\nexport function setupImageOptimization() {\n // Add loading=\"lazy\" to all images that don't have it\n const images = document.querySelectorAll('img:not([loading])');\n images.forEach((img) => {\n img.setAttribute('loading', 'lazy');\n });\n}\n\n// Preconnect to external domains for faster resource loading\nexport function setupPreconnects() {\n const preconnects = [\n 'https://fonts.googleapis.com',\n 'https://fonts.gstatic.com',\n ];\n\n preconnects.forEach((url) => {\n const link = document.createElement('link');\n link.rel = 'preconnect';\n link.href = url;\n link.crossOrigin = 'anonymous';\n document.head.appendChild(link);\n });\n}\n\n// Setup performance monitoring\nexport function setupPerformanceMonitoring() {\n // Monitor Core Web Vitals\n if ('web-vital' in window || typeof window !== 'undefined') {\n // Track First Contentful Paint\n const observer = new PerformanceObserver((list) => {\n for (const entry of list.getEntries()) {\n if (entry.name === 'first-contentful-paint') {\n console.log('FCP:', entry.startTime);\n }\n }\n });\n \n try {\n observer.observe({ entryTypes: ['paint'] });\n } catch (e) {\n // Browser doesn't support this API\n }\n }\n}\n\n// Debounce function for performance\nexport function debounce<T extends (...args: any[]) => any>(\n func: T,\n wait: number\n): (...args: Parameters<T>) => void {\n let timeout: NodeJS.Timeout;\n return (...args: Parameters<T>) => {\n clearTimeout(timeout);\n timeout = setTimeout(() => func(...args), wait);\n };\n}\n\n// Throttle function for performance\nexport function throttle<T extends (...args: any[]) => any>(\n func: T,\n limit: number\n): (...args: Parameters<T>) => void {\n let inThrottle = false;\n return (...args: Parameters<T>) => {\n if (!inThrottle) {\n func(...args);\n inThrottle = true;\n setTimeout(() => inThrottle = false, limit);\n }\n };\n}","size_bytes":2812},"client/src/components/layout/footer.tsx":{"content":"import { useQuery } from \"@tanstack/react-query\";\n\ninterface VersionInfo {\n version: string;\n name: string;\n build: string;\n release: string;\n}\n\nexport default function Footer() {\n const { data: versionInfo } = useQuery<VersionInfo>({\n queryKey: [\"/api/version\"],\n retry: false,\n });\n\n return (\n <footer className=\"bg-white dark:bg-gray-800 amoled:bg-black border-t border-gray-200 dark:border-gray-700 amoled:border-gray-800 mt-auto\">\n <div className=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6\">\n <div className=\"flex flex-col md:flex-row items-center justify-between space-y-4 md:space-y-0\">\n {/* Left side - Branding */}\n <div className=\"flex items-center space-x-2\">\n <i className=\"material-icons text-primary-700 text-xl\">build</i>\n <div>\n <h3 className=\"text-sm font-medium text-gray-900 dark:text-white amoled:text-white\">\n JCR Guide Pro\n </h3>\n <p className=\"text-xs text-gray-500 dark:text-gray-400\">\n Professional repair guides for everyone\n </p>\n </div>\n </div>\n\n {/* Center - Quick Links */}\n <div className=\"flex items-center space-x-6 text-sm\">\n <a \n href=\"/troubleshooting\" \n className=\"text-gray-600 dark:text-gray-300 hover:text-primary-700 dark:hover:text-primary-400 transition-colors\"\n >\n Help\n </a>\n <a \n href=\"/software-tools\" \n className=\"text-gray-600 dark:text-gray-300 hover:text-primary-700 dark:hover:text-primary-400 transition-colors\"\n >\n Tools\n </a>\n <a \n href=\"/create-guide\" \n className=\"text-gray-600 dark:text-gray-300 hover:text-primary-700 dark:hover:text-primary-400 transition-colors\"\n >\n Contribute\n </a>\n <a \n href=\"mailto:JCRguideproofficial@gmail.com\" \n className=\"text-gray-600 dark:text-gray-300 hover:text-primary-700 dark:hover:text-primary-400 transition-colors\"\n >\n Contact\n </a>\n </div>\n\n {/* Right side - Version Info */}\n <div className=\"text-center md:text-right\">\n {versionInfo && (\n <div className=\"text-xs text-gray-500 dark:text-gray-400\">\n <div>Version {versionInfo.version}</div>\n <div>Build {versionInfo.build}</div>\n </div>\n )}\n <div className=\"text-xs text-gray-400 dark:text-gray-500 mt-1\">\n © 2025 JCR Guide Pro\n </div>\n </div>\n </div>\n </div>\n </footer>\n );\n}","size_bytes":2768},"client/src/components/layout/header.tsx":{"content":"import { useState } from \"react\";\nimport { useLocation } from \"wouter\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { Input } from \"@/components/ui/input\";\nimport { Button } from \"@/components/ui/button\";\nimport { MobileMenu } from \"@/components/layout/mobile-menu\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n DropdownMenuLabel,\n} from \"@/components/ui/dropdown-menu\";\nimport { useAuth } from \"@/hooks/useAuth\";\nimport { useTheme } from \"@/contexts/theme-context\";\nimport { ChevronDown } from \"lucide-react\";\nimport type { RepairGuide, User } from \"@shared/schema\";\n\nexport default function Header() {\n const [, setLocation] = useLocation();\n const [searchQuery, setSearchQuery] = useState(\"\");\n const [showResults, setShowResults] = useState(false);\n const { user, isAuthenticated } = useAuth() as { user: User | undefined; isAuthenticated: boolean };\n const { setTheme } = useTheme();\n\n const { data: searchResults = [] } = useQuery<RepairGuide[]>({\n queryKey: [\"/api/repair-guides/search\", searchQuery],\n queryFn: () => fetch(`/api/repair-guides/search?q=${encodeURIComponent(searchQuery)}`).then(res => res.json()),\n enabled: searchQuery.length > 2,\n });\n\n const handleSearch = (e: React.FormEvent) => {\n e.preventDefault();\n if (searchQuery.trim()) {\n setShowResults(false);\n // Navigate to home with search results\n setLocation(`/?search=${encodeURIComponent(searchQuery)}`);\n }\n };\n\n\n\n return (\n <header className=\"bg-white dark:bg-gray-800 amoled:bg-black shadow-sm border-b border-gray-200 dark:border-gray-700 amoled:border-gray-800 sticky top-0 z-50\">\n <div className=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8\">\n <div className=\"flex items-center justify-between h-16\">\n {/* Logo and Brand */}\n <div className=\"flex items-center space-x-4\">\n <MobileMenu />\n <button \n onClick={() => setLocation(\"/\")}\n className=\"flex items-center space-x-2 hover:opacity-80 transition-opacity\"\n >\n <i className=\"material-icons text-primary-700 text-2xl\">build</i>\n <h1 className=\"text-xl font-medium text-gray-900 dark:text-white amoled:text-white\">JCR Guide Pro</h1>\n </button>\n \n {/* Navigation */}\n <nav className=\"hidden md:flex items-center space-x-6 ml-8\">\n <button\n onClick={() => setLocation(\"/\")}\n className=\"text-sm text-gray-600 dark:text-gray-300 hover:text-primary-700 dark:hover:text-primary-400 transition-colors\"\n >\n Guides\n </button>\n\n {/* Tools & Content Dropdown */}\n <DropdownMenu>\n <DropdownMenuTrigger className=\"flex items-center gap-1 text-sm text-gray-600 dark:text-gray-300 hover:text-primary-700 dark:hover:text-primary-400 transition-colors\">\n Tools & Content\n <ChevronDown className=\"h-4 w-4\" />\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\" className=\"w-52\">\n <DropdownMenuLabel>Repair Tools</DropdownMenuLabel>\n <DropdownMenuItem onClick={() => setLocation(\"/troubleshooting\")}>\n <i className=\"material-icons text-sm mr-2\">help_outline</i>\n Troubleshooting\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => setLocation(\"/device-finder\")}>\n <i className=\"material-icons text-sm mr-2\">search</i>\n Device Finder\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => setLocation(\"/software-tools\")}>\n <i className=\"material-icons text-sm mr-2\">build_circle</i>\n Software Tools\n </DropdownMenuItem>\n \n <DropdownMenuSeparator />\n \n <DropdownMenuLabel>Support</DropdownMenuLabel>\n <DropdownMenuItem onClick={() => window.open('mailto:JCRguideproofficial@gmail.com', '_blank')}>\n <i className=\"material-icons text-sm mr-2\">email</i>\n Contact Support\n </DropdownMenuItem>\n \n {user?.role === \"admin\" && (\n <>\n <DropdownMenuSeparator />\n <DropdownMenuLabel>Administrator</DropdownMenuLabel>\n <DropdownMenuItem onClick={() => setLocation(\"/admin\")}>\n <i className=\"material-icons text-sm mr-2\">admin_panel_settings</i>\n Admin Dashboard\n </DropdownMenuItem>\n </>\n )}\n \n <DropdownMenuSeparator />\n \n <DropdownMenuLabel>Content & Storage</DropdownMenuLabel>\n <DropdownMenuItem onClick={() => setLocation(\"/create-guide\")}>\n <i className=\"material-icons text-sm mr-2\">add_circle</i>\n Create Guide\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => setLocation(\"/downloads\")}>\n <i className=\"material-icons text-sm mr-2\">download</i>\n Downloads\n </DropdownMenuItem>\n\n <DropdownMenuItem onClick={() => setLocation(\"/offline-settings\")}>\n <i className=\"material-icons text-sm mr-2\">cloud_off</i>\n Offline Mode\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n </nav>\n </div>\n\n {/* Search Bar */}\n <div className=\"flex-1 max-w-3xl mx-8 relative\">\n <form onSubmit={handleSearch}>\n <div className=\"relative\">\n <div className=\"absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none\">\n <i className=\"material-icons text-gray-400 dark:text-gray-500 amoled:text-gray-400 text-xl\">search</i>\n </div>\n <Input\n type=\"text\"\n value={searchQuery}\n onChange={(e) => {\n setSearchQuery(e.target.value);\n setShowResults(e.target.value.length > 2);\n }}\n onBlur={() => setTimeout(() => setShowResults(false), 200)}\n onFocus={() => setShowResults(searchQuery.length > 2)}\n className=\"block w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-gray-600 amoled:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-700 amoled:bg-gray-900 text-sm text-gray-900 dark:text-white amoled:text-white placeholder-gray-500 dark:placeholder-gray-400 amoled:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent\"\n placeholder=\"Search repair guides, issues, or parts...\"\n />\n </div>\n </form>\n\n {/* Search Results Dropdown */}\n {showResults && searchResults.length > 0 && (\n <div className=\"absolute top-full left-0 right-0 mt-1 bg-white dark:bg-gray-800 amoled:bg-gray-900 border border-gray-200 dark:border-gray-700 amoled:border-gray-700 rounded-lg shadow-material-lg z-50 max-h-96 overflow-y-auto\">\n {searchResults.slice(0, 5).map((guide) => (\n <button\n key={guide.id}\n onClick={() => {\n setLocation(`/guide/${guide.id}`);\n setShowResults(false);\n }}\n className=\"w-full px-4 py-3 text-left hover:bg-gray-50 dark:hover:bg-gray-700 amoled:hover:bg-gray-800 border-b border-gray-100 dark:border-gray-600 amoled:border-gray-700 last:border-b-0\"\n >\n <div className=\"font-medium text-gray-900 dark:text-white amoled:text-white text-sm\">{guide.title}</div>\n <div className=\"text-xs text-gray-500 dark:text-gray-400 amoled:text-gray-400 mt-1\">\n {guide.category} • {guide.difficulty} • {guide.estimatedTime}\n </div>\n </button>\n ))}\n </div>\n )}\n </div>\n\n {/* User Actions */}\n <div className=\"flex items-center space-x-2\">\n {/* Favorites Button */}\n {isAuthenticated && (\n <Button\n onClick={() => setLocation(\"/favorites\")}\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-9 px-3 text-gray-600 dark:text-gray-300 hover:text-primary-700 dark:hover:text-primary-400 transition-colors\"\n >\n <i className=\"material-icons text-sm mr-2\">favorite</i>\n <span className=\"hidden sm:inline\">Favorites</span>\n </Button>\n )}\n\n\n {/* Combined User & Theme Menu */}\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button variant=\"ghost\" size=\"sm\" className=\"h-9 px-3\">\n {isAuthenticated ? (\n <div className=\"flex items-center space-x-2\">\n {user?.profileImageUrl ? (\n <img\n src={user.profileImageUrl}\n alt=\"Profile\"\n className=\"w-6 h-6 rounded-full object-cover\"\n />\n ) : (\n <div className=\"w-6 h-6 bg-primary-100 dark:bg-primary-900 rounded-full flex items-center justify-center\">\n <i className=\"material-icons text-primary-700 dark:text-primary-300 text-sm\">person</i>\n </div>\n )}\n <span className=\"text-sm hidden lg:block max-w-32 truncate\">\n {user?.firstName ? `${user.firstName} ${user?.lastName || ''}`.trim() : user?.email}\n </span>\n <ChevronDown className=\"h-4 w-4\" />\n </div>\n ) : (\n <div className=\"flex items-center space-x-2\">\n <i className=\"material-icons text-base\">account_circle</i>\n <span className=\"text-sm\">Account</span>\n <ChevronDown className=\"h-4 w-4\" />\n </div>\n )}\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" className=\"w-56\">\n {isAuthenticated ? (\n <>\n {/* User Info */}\n <div className=\"px-3 py-2 border-b\">\n <div className=\"flex items-center space-x-3\">\n {user?.profileImageUrl ? (\n <img\n src={user.profileImageUrl}\n alt=\"Profile\"\n className=\"w-10 h-10 rounded-full object-cover\"\n />\n ) : (\n <div className=\"w-10 h-10 bg-primary-100 dark:bg-primary-900 rounded-full flex items-center justify-center\">\n <i className=\"material-icons text-primary-700 dark:text-primary-300 text-lg\">person</i>\n </div>\n )}\n <div className=\"flex-1 min-w-0\">\n <p className=\"text-sm font-medium text-gray-900 dark:text-white truncate\">\n {user?.firstName ? `${user.firstName} ${user?.lastName || ''}`.trim() : 'User'}\n </p>\n {user?.email && (\n <p className=\"text-xs text-gray-500 dark:text-gray-400 truncate\">\n {user.email}\n </p>\n )}\n </div>\n </div>\n </div>\n \n <DropdownMenuSeparator />\n \n {/* Theme Section */}\n <DropdownMenuLabel>Appearance</DropdownMenuLabel>\n <DropdownMenuItem onClick={() => setTheme(\"light\")}>\n <i className=\"material-icons mr-2 text-base\">light_mode</i>\n <div className=\"flex flex-col flex-1\">\n <span>Light Theme</span>\n <span className=\"text-xs text-muted-foreground\">Bright and clean</span>\n </div>\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => setTheme(\"dark\")}>\n <i className=\"material-icons mr-2 text-base\">dark_mode</i>\n <div className=\"flex flex-col flex-1\">\n <span>Dark Theme</span>\n <span className=\"text-xs text-muted-foreground\">Easy on the eyes</span>\n </div>\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => setTheme(\"amoled\")}>\n <i className=\"material-icons mr-2 text-base\">phone_android</i>\n <div className=\"flex flex-col flex-1\">\n <span>AMOLED Theme</span>\n <span className=\"text-xs text-muted-foreground\">Pure black for OLED</span>\n </div>\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => setTheme(\"system\")}>\n <i className=\"material-icons mr-2 text-base\">computer</i>\n <div className=\"flex flex-col flex-1\">\n <span>System Theme</span>\n <span className=\"text-xs text-muted-foreground\">Follows device setting</span>\n </div>\n </DropdownMenuItem>\n \n <DropdownMenuSeparator />\n \n {/* Sign Out */}\n <DropdownMenuItem\n onClick={() => {\n window.location.href = \"/api/logout\";\n }}\n className=\"text-red-600 dark:text-red-400 focus:text-red-600 dark:focus:text-red-400\"\n >\n <i className=\"material-icons mr-2 text-base\">logout</i>\n Sign Out\n </DropdownMenuItem>\n </>\n ) : (\n <>\n {/* Theme Section for Non-Authenticated Users */}\n <DropdownMenuLabel>Appearance</DropdownMenuLabel>\n <DropdownMenuItem onClick={() => setTheme(\"light\")}>\n <i className=\"material-icons mr-2 text-base\">light_mode</i>\n <div className=\"flex flex-col flex-1\">\n <span>Light Theme</span>\n <span className=\"text-xs text-muted-foreground\">Bright and clean</span>\n </div>\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => setTheme(\"dark\")}>\n <i className=\"material-icons mr-2 text-base\">dark_mode</i>\n <div className=\"flex flex-col flex-1\">\n <span>Dark Theme</span>\n <span className=\"text-xs text-muted-foreground\">Easy on the eyes</span>\n </div>\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => setTheme(\"amoled\")}>\n <i className=\"material-icons mr-2 text-base\">phone_android</i>\n <div className=\"flex flex-col flex-1\">\n <span>AMOLED Theme</span>\n <span className=\"text-xs text-muted-foreground\">Pure black for OLED</span>\n </div>\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => setTheme(\"system\")}>\n <i className=\"material-icons mr-2 text-base\">computer</i>\n <div className=\"flex flex-col flex-1\">\n <span>System Theme</span>\n <span className=\"text-xs text-muted-foreground\">Follows device setting</span>\n </div>\n </DropdownMenuItem>\n \n <DropdownMenuSeparator />\n \n {/* Sign In */}\n <DropdownMenuItem\n onClick={() => {\n window.location.href = \"/api/login\";\n }}\n className=\"text-primary-600 dark:text-primary-400 focus:text-primary-600 dark:focus:text-primary-400\"\n >\n <i className=\"material-icons mr-2 text-base\">login</i>\n Sign In to JCR Guide Pro\n </DropdownMenuItem>\n </>\n )}\n </DropdownMenuContent>\n </DropdownMenu>\n </div>\n </div>\n </div>\n </header>\n );\n}\n","size_bytes":17465},"client/src/components/layout/mobile-menu.tsx":{"content":"import { useState } from \"react\";\nimport { useLocation } from \"wouter\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n Sheet,\n SheetContent,\n SheetHeader,\n SheetTitle,\n SheetTrigger,\n} from \"@/components/ui/sheet\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { useAuth } from \"@/hooks/useAuth\";\nimport type { User } from \"@shared/schema\";\n\nexport function MobileMenu() {\n const [open, setOpen] = useState(false);\n const [, setLocation] = useLocation();\n const { user, isAuthenticated } = useAuth() as { user: User | undefined; isAuthenticated: boolean };\n\n const handleNavigation = (path: string) => {\n setLocation(path);\n setOpen(false);\n };\n\n return (\n <Sheet open={open} onOpenChange={setOpen}>\n <SheetTrigger asChild>\n <Button variant=\"ghost\" size=\"icon\" className=\"md:hidden\">\n <i className=\"material-icons\">menu</i>\n <span className=\"sr-only\">Toggle menu</span>\n </Button>\n </SheetTrigger>\n <SheetContent side=\"left\" className=\"w-80\">\n <SheetHeader>\n <SheetTitle className=\"flex items-center space-x-2\">\n <i className=\"material-icons text-primary-700 text-xl\">build</i>\n <span>JCR Guide Pro</span>\n </SheetTitle>\n </SheetHeader>\n \n <div className=\"mt-6 space-y-6\">\n {/* Main Navigation */}\n <div className=\"space-y-3\">\n <h3 className=\"text-sm font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wide\">\n Navigation\n </h3>\n <div className=\"space-y-1\">\n <Button\n variant=\"ghost\"\n className=\"w-full justify-start\"\n onClick={() => handleNavigation(\"/\")}\n >\n <i className=\"material-icons mr-3 text-lg\">home</i>\n Repair Guides\n </Button>\n \n {isAuthenticated && (\n <Button\n variant=\"ghost\"\n className=\"w-full justify-start\"\n onClick={() => handleNavigation(\"/favorites\")}\n >\n <i className=\"material-icons mr-3 text-lg\">favorite</i>\n Favorites\n </Button>\n )}\n </div>\n </div>\n\n <Separator />\n\n {/* Tools & Content */}\n <div className=\"space-y-3\">\n <h3 className=\"text-sm font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wide\">\n Tools & Content\n </h3>\n <div className=\"space-y-1\">\n <Button\n variant=\"ghost\"\n className=\"w-full justify-start\"\n onClick={() => handleNavigation(\"/troubleshooting\")}\n >\n <i className=\"material-icons mr-3 text-lg\">help_outline</i>\n Troubleshooting\n </Button>\n <Button\n variant=\"ghost\"\n className=\"w-full justify-start\"\n onClick={() => handleNavigation(\"/device-finder\")}\n >\n <i className=\"material-icons mr-3 text-lg\">search</i>\n Device Finder\n </Button>\n <Button\n variant=\"ghost\"\n className=\"w-full justify-start\"\n onClick={() => handleNavigation(\"/software-tools\")}\n >\n <i className=\"material-icons mr-3 text-lg\">build_circle</i>\n Software Tools\n </Button>\n <Button\n variant=\"ghost\"\n className=\"w-full justify-start\"\n onClick={() => handleNavigation(\"/create-guide\")}\n >\n <i className=\"material-icons mr-3 text-lg\">add_circle</i>\n Create Guide\n </Button>\n <Button\n variant=\"ghost\"\n className=\"w-full justify-start\"\n onClick={() => handleNavigation(\"/downloads\")}\n >\n <i className=\"material-icons mr-3 text-lg\">download</i>\n Downloads\n </Button>\n\n <Button\n variant=\"ghost\"\n className=\"w-full justify-start\"\n onClick={() => handleNavigation(\"/offline-settings\")}\n >\n <i className=\"material-icons mr-3 text-lg\">cloud_off</i>\n Offline Mode\n </Button>\n </div>\n </div>\n\n {/* Authentication */}\n {!isAuthenticated ? (\n <>\n <Separator />\n <div className=\"space-y-3\">\n <Button\n onClick={() => {\n window.location.href = \"/api/login\";\n }}\n className=\"w-full\"\n >\n <i className=\"material-icons mr-2 text-lg\">login</i>\n Sign In\n </Button>\n </div>\n </>\n ) : (\n <>\n <Separator />\n <div className=\"space-y-3\">\n <div className=\"flex items-center space-x-3 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg\">\n {user?.profileImageUrl ? (\n <img\n src={user.profileImageUrl}\n alt=\"Profile\"\n className=\"w-8 h-8 rounded-full object-cover\"\n />\n ) : (\n <div className=\"w-8 h-8 bg-primary-100 dark:bg-primary-900 rounded-full flex items-center justify-center\">\n <i className=\"material-icons text-primary-700 dark:text-primary-300 text-lg\">person</i>\n </div>\n )}\n <div className=\"flex-1 min-w-0\">\n <p className=\"text-sm font-medium text-gray-900 dark:text-white truncate\">\n {user?.firstName ? `${user.firstName} ${user?.lastName || ''}`.trim() : user?.email}\n </p>\n {user?.email && user?.firstName && (\n <p className=\"text-xs text-gray-500 dark:text-gray-400 truncate\">\n {user.email}\n </p>\n )}\n </div>\n </div>\n <Button\n variant=\"outline\"\n onClick={() => {\n window.location.href = \"/api/logout\";\n }}\n className=\"w-full\"\n >\n <i className=\"material-icons mr-2 text-lg\">logout</i>\n Sign Out\n </Button>\n </div>\n </>\n )}\n </div>\n </SheetContent>\n </Sheet>\n );\n}","size_bytes":6810},"client/src/components/repair/category-grid.tsx":{"content":"import { useQuery } from \"@tanstack/react-query\";\nimport type { RepairGuide } from \"@shared/schema\";\n\ninterface CategoryGridProps {\n selectedCategory: string;\n onCategoryChange: (category: string) => void;\n deviceType: string;\n}\n\nexport default function CategoryGrid({ selectedCategory, onCategoryChange, deviceType }: CategoryGridProps) {\n const { data: allGuides = [] } = useQuery<RepairGuide[]>({\n queryKey: [\"/api/repair-guides\", deviceType],\n queryFn: () => {\n const params = new URLSearchParams();\n if (deviceType) params.append('deviceType', deviceType);\n return fetch(`/api/repair-guides?${params}`).then(res => res.json());\n },\n });\n\n const categories = [\n {\n id: \"hardware\",\n name: \"Hardware\",\n description: \"Component repairs, replacements, and installations\",\n icon: \"memory\",\n color: \"text-primary-700\",\n bgColor: \"bg-primary-50\",\n },\n {\n id: \"software\",\n name: \"Software\",\n description: \"OS troubleshooting, driver issues, and system recovery\",\n icon: \"code\",\n color: \"text-success-600\",\n bgColor: \"bg-success-50\",\n },\n {\n id: \"cleaning\",\n name: \"Cleaning\",\n description: \"Dust removal, thermal paste, and maintenance\",\n icon: \"cleaning_services\",\n color: \"text-warning-700\",\n bgColor: \"bg-warning-50\",\n },\n {\n id: \"upgrades\",\n name: \"Upgrades\",\n description: \"RAM, storage, and performance improvements\",\n icon: \"upgrade\",\n color: \"text-purple-600\",\n bgColor: \"bg-purple-50\",\n },\n ];\n\n const getCategoryCount = (categoryId: string) => {\n return allGuides.filter(guide => guide.category === categoryId).length;\n };\n\n return (\n <section>\n <div className=\"flex items-center justify-between mb-4\">\n <h2 className=\"text-lg font-medium text-gray-900 dark:text-white\">Repair Categories</h2>\n {selectedCategory && (\n <button\n onClick={() => onCategoryChange(\"\")}\n className=\"text-sm text-primary-700 hover:text-primary-900 transition-colors\"\n >\n Clear filter\n </button>\n )}\n </div>\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4\">\n {categories.map((category) => (\n <button\n key={category.id}\n onClick={() => onCategoryChange(selectedCategory === category.id ? \"\" : category.id)}\n className={`bg-white dark:bg-gray-800 rounded-xl shadow-material p-6 hover:shadow-material-lg transition-all cursor-pointer text-left ${\n selectedCategory === category.id ? \"ring-2 ring-primary ring-offset-2\" : \"\"\n }`}\n >\n <div className=\"flex items-center space-x-3 mb-3\">\n <i className={`material-icons ${category.color} text-2xl`}>{category.icon}</i>\n <h3 className=\"font-medium text-gray-900 dark:text-white\">{category.name}</h3>\n </div>\n <p className=\"text-sm text-gray-600 dark:text-gray-300 mb-2\">{category.description}</p>\n <span className={`inline-block px-2 py-1 ${category.bgColor} ${category.color} text-xs rounded-full`}>\n {getCategoryCount(category.id)} guides\n </span>\n </button>\n ))}\n </div>\n </section>\n );\n}\n","size_bytes":3317},"client/src/components/repair/device-selector.tsx":{"content":"import { Button } from \"@/components/ui/button\";\n\ninterface DeviceSelectorProps {\n selectedType: \"laptop\" | \"desktop\";\n onTypeChange: (type: \"laptop\" | \"desktop\") => void;\n}\n\nexport default function DeviceSelector({ selectedType, onTypeChange }: DeviceSelectorProps) {\n return (\n <div className=\"bg-white dark:bg-gray-800 amoled:bg-black rounded-xl shadow-material p-6\">\n <h2 className=\"text-lg font-medium text-gray-900 dark:text-white amoled:text-white mb-4\">Select Device Type</h2>\n <div className=\"flex space-x-4\">\n <Button\n onClick={() => onTypeChange(\"laptop\")}\n className={`flex items-center space-x-3 px-6 py-4 rounded-lg transition-colors ${\n selectedType === \"laptop\"\n ? \"bg-primary-700 text-white hover:bg-primary-900\"\n : \"bg-gray-100 text-gray-700 hover:bg-gray-200\"\n }`}\n >\n <i className=\"material-icons text-2xl\">laptop_mac</i>\n <span className=\"font-medium\">Laptops</span>\n </Button>\n <Button\n onClick={() => onTypeChange(\"desktop\")}\n className={`flex items-center space-x-3 px-6 py-4 rounded-lg transition-colors ${\n selectedType === \"desktop\"\n ? \"bg-primary-700 text-white hover:bg-primary-900\"\n : \"bg-gray-100 text-gray-700 hover:bg-gray-200\"\n }`}\n >\n <i className=\"material-icons text-2xl\">desktop_windows</i>\n <span className=\"font-medium\">Desktops</span>\n </Button>\n </div>\n </div>\n );\n}\n","size_bytes":1540},"client/src/components/repair/download-button.tsx":{"content":"import { useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { DownloadModal } from \"@/components/ui/download-modal\";\nimport { Download, FileDown } from \"lucide-react\";\nimport type { RepairGuide } from \"@shared/schema\";\n\ninterface DownloadButtonProps {\n guide: RepairGuide;\n variant?: \"icon\" | \"button\";\n size?: \"sm\" | \"md\" | \"lg\";\n videoUrl?: string;\n}\n\nexport default function DownloadButton({ \n guide, \n variant = \"icon\", \n size = \"sm\",\n videoUrl \n}: DownloadButtonProps) {\n const [showDownloadModal, setShowDownloadModal] = useState(false);\n\n if (variant === \"button\") {\n return (\n <>\n <Button\n onClick={(e) => {\n e.stopPropagation();\n setShowDownloadModal(true);\n }}\n className={`flex items-center gap-2 ${\n size === \"sm\" ? \"px-3 py-1.5 text-sm\" : \n size === \"md\" ? \"px-4 py-2\" : \n \"px-6 py-3 text-lg\"\n }`}\n >\n <FileDown className={size === \"sm\" ? \"h-4 w-4\" : size === \"md\" ? \"h-5 w-5\" : \"h-6 w-6\"} />\n Download\n </Button>\n \n <DownloadModal\n open={showDownloadModal}\n onOpenChange={setShowDownloadModal}\n guide={guide}\n videoUrl={videoUrl}\n />\n </>\n );\n }\n\n return (\n <>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={(e) => {\n e.stopPropagation();\n setShowDownloadModal(true);\n }}\n className=\"text-gray-500 hover:text-primary-700 transition-colors\"\n title=\"Download guide\"\n >\n <Download className={size === \"sm\" ? \"h-4 w-4\" : size === \"md\" ? \"h-5 w-5\" : \"h-6 w-6\"} />\n </Button>\n \n <DownloadModal\n open={showDownloadModal}\n onOpenChange={setShowDownloadModal}\n guide={guide}\n videoUrl={videoUrl}\n />\n </>\n );\n}","size_bytes":1892},"client/src/components/repair/floating-action-button.tsx":{"content":"import { useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\n\nexport default function FloatingActionButton() {\n const [isExpanded, setIsExpanded] = useState(false);\n\n const quickActions = [\n {\n id: \"emergency\",\n label: \"Emergency Shutdown\",\n icon: \"emergency\",\n color: \"text-danger-500\",\n action: () => alert(\"Emergency shutdown guide: Hold power button for 10 seconds to force shutdown.\"),\n },\n {\n id: \"tools\",\n label: \"Tool Checklist\",\n icon: \"construction\",\n color: \"text-primary-700\",\n action: () => alert(\"Basic tool checklist: Phillips screwdriver, anti-static wrist strap, compressed air.\"),\n },\n {\n id: \"safety\",\n label: \"Safety Guide\",\n icon: \"security\",\n color: \"text-warning-700\",\n action: () => alert(\"Safety reminders: Power down device, ground yourself, work in clean environment.\"),\n },\n {\n id: \"software\",\n label: \"Download Tools\",\n icon: \"download\",\n color: \"text-cyan-600\",\n action: () => {\n const softwareSection = document.querySelector('[data-section=\"software-tools\"]') || \n document.querySelector('section:last-of-type');\n if (softwareSection) {\n softwareSection.scrollIntoView({ behavior: 'smooth' });\n }\n },\n },\n ];\n\n const handleQuickAction = (action: () => void) => {\n action();\n setIsExpanded(false);\n };\n\n return (\n <div className=\"fixed bottom-6 right-6 z-40 no-print\">\n <div className=\"relative\">\n {/* Quick access menu */}\n <div\n className={`absolute bottom-16 right-0 space-y-2 transition-all duration-200 ${\n isExpanded ? \"opacity-100 pointer-events-auto\" : \"opacity-0 pointer-events-none\"\n }`}\n >\n {quickActions.map((item) => (\n <Button\n key={item.id}\n onClick={() => handleQuickAction(item.action)}\n className=\"flex items-center space-x-3 bg-white shadow-material rounded-full px-4 py-3 hover:shadow-material-lg transition-shadow text-gray-700 hover:text-gray-900\"\n variant=\"outline\"\n >\n <i className={`material-icons ${item.color}`}>{item.icon}</i>\n <span className=\"text-sm font-medium whitespace-nowrap\">{item.label}</span>\n </Button>\n ))}\n </div>\n\n {/* Main FAB */}\n <Button\n onClick={() => setIsExpanded(!isExpanded)}\n className=\"w-14 h-14 bg-primary-700 text-white rounded-full shadow-material-lg hover:bg-primary-900 hover:shadow-xl transition-all duration-200 flex items-center justify-center\"\n >\n <i className={`material-icons text-2xl transition-transform ${isExpanded ? \"rotate-45\" : \"\"}`}>\n {isExpanded ? \"close\" : \"help\"}\n </i>\n </Button>\n </div>\n </div>\n );\n}\n","size_bytes":2911},"client/src/components/repair/guide-card.tsx":{"content":"import { useState } from \"react\";\nimport { useMutation, useQueryClient } from \"@tanstack/react-query\";\nimport { Button } from \"@/components/ui/button\";\nimport { DownloadModal } from \"@/components/ui/download-modal\";\nimport { apiRequest } from \"@/lib/queryClient\";\nimport { Download } from \"lucide-react\";\nimport type { RepairGuide } from \"@shared/schema\";\n\ninterface GuideCardProps {\n guide: RepairGuide;\n onViewGuide: () => void;\n}\n\nexport default function GuideCard({ guide, onViewGuide }: GuideCardProps) {\n const queryClient = useQueryClient();\n const [showDownloadModal, setShowDownloadModal] = useState(false);\n\n const bookmarkMutation = useMutation({\n mutationFn: () => apiRequest(\"PATCH\", `/api/repair-guides/${guide.id}/bookmark`),\n onSuccess: () => {\n queryClient.invalidateQueries({ queryKey: [\"/api/repair-guides\"] });\n },\n });\n\n const getDifficultyColor = (difficulty: string) => {\n switch (difficulty) {\n case \"easy\":\n return \"bg-success-50 text-success-600\";\n case \"medium\":\n return \"bg-warning-50 text-warning-700\";\n case \"hard\":\n return \"bg-danger-50 text-danger-700\";\n default:\n return \"bg-gray-50 text-gray-600\";\n }\n };\n\n const formatViewCount = (count: number) => {\n if (count >= 1000) {\n return `${(count / 1000).toFixed(1)}k`;\n }\n return count.toString();\n };\n\n return (\n <div className=\"bg-white dark:bg-gray-800 amoled:bg-black rounded-xl shadow-material hover:shadow-material-lg transition-shadow\">\n <img\n src={guide.imageUrl}\n alt={guide.title}\n className=\"w-full h-48 object-cover rounded-t-xl\"\n />\n\n <div className=\"p-6\">\n <div className=\"flex items-center justify-between mb-3\">\n <div className=\"flex items-center space-x-2\">\n <span className={`inline-block px-2 py-1 text-xs font-medium rounded-full ${getDifficultyColor(guide.difficulty)}`}>\n {guide.difficulty.toUpperCase()}\n </span>\n <span className=\"inline-block px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded-full\">\n {guide.estimatedTime}\n </span>\n </div>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={(e) => {\n e.stopPropagation();\n bookmarkMutation.mutate();\n }}\n disabled={bookmarkMutation.isPending}\n className=\"p-1 text-gray-400 hover:text-primary-700 transition-colors\"\n >\n <i className={`material-icons text-sm ${guide.isBookmarked ? \"text-primary-700\" : \"\"}`}>\n {guide.isBookmarked ? \"bookmark\" : \"bookmark_border\"}\n </i>\n </Button>\n </div>\n\n <h3 className=\"font-medium text-gray-900 dark:text-white amoled:text-white mb-2\">{guide.title}</h3>\n <p className=\"text-sm text-gray-600 dark:text-gray-300 amoled:text-gray-300 mb-4 line-clamp-2\">{guide.description}</p>\n\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center space-x-4 text-sm text-gray-500\">\n <span className=\"flex items-center space-x-1\">\n <i className=\"material-icons text-sm\">build</i>\n <span>{guide.toolsRequired.length} tools</span>\n </span>\n <span className=\"flex items-center space-x-1\">\n <i className=\"material-icons text-sm\">visibility</i>\n <span>{formatViewCount(guide.viewCount || 0)} views</span>\n </span>\n <span className=\"flex items-center space-x-1\">\n <i className=\"material-icons text-sm\">download</i>\n <span>{formatViewCount(guide.downloadCount || 0)} downloads</span>\n </span>\n </div>\n <div className=\"flex items-center space-x-2\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={(e) => {\n e.stopPropagation();\n setShowDownloadModal(true);\n }}\n className=\"text-gray-500 hover:text-primary-700 p-1\"\n >\n <Download className=\"h-4 w-4\" />\n </Button>\n <Button\n onClick={onViewGuide}\n className=\"px-4 py-2 bg-primary-700 text-white text-sm rounded-lg hover:bg-primary-900 transition-colors\"\n >\n View Guide\n </Button>\n </div>\n </div>\n </div>\n \n <DownloadModal\n open={showDownloadModal}\n onOpenChange={setShowDownloadModal}\n guide={guide}\n />\n </div>\n );\n}\n","size_bytes":4615},"client/src/components/repair/guide-detail-modal.tsx":{"content":"import { useQuery, useMutation, useQueryClient } from \"@tanstack/react-query\";\nimport { Button } from \"@/components/ui/button\";\nimport { Dialog, DialogContent, DialogHeader, DialogTitle } from \"@/components/ui/dialog\";\nimport { apiRequest } from \"@/lib/queryClient\";\nimport type { RepairGuide } from \"@shared/schema\";\n\ninterface GuideDetailModalProps {\n guideId: string;\n onClose: () => void;\n isPage?: boolean;\n}\n\nexport default function GuideDetailModal({ guideId, onClose, isPage = false }: GuideDetailModalProps) {\n const queryClient = useQueryClient();\n\n const { data: guide, isLoading, error } = useQuery<RepairGuide>({\n queryKey: [\"/api/repair-guides\", guideId],\n enabled: !!guideId,\n });\n\n const bookmarkMutation = useMutation({\n mutationFn: () => apiRequest(\"PATCH\", `/api/repair-guides/${guideId}/bookmark`),\n onSuccess: () => {\n queryClient.invalidateQueries({ queryKey: [\"/api/repair-guides\"] });\n },\n });\n\n if (isLoading) {\n return (\n <Dialog open={true} onOpenChange={() => onClose()}>\n <DialogContent className=\"max-w-4xl max-h-[90vh] overflow-y-auto\">\n <div className=\"animate-pulse space-y-4\">\n <div className=\"h-8 bg-gray-200 rounded w-3/4\"></div>\n <div className=\"h-4 bg-gray-200 rounded w-full\"></div>\n <div className=\"h-4 bg-gray-200 rounded w-5/6\"></div>\n <div className=\"space-y-3\">\n {[...Array(3)].map((_, i) => (\n <div key={i} className=\"h-24 bg-gray-200 rounded\"></div>\n ))}\n </div>\n </div>\n </DialogContent>\n </Dialog>\n );\n }\n\n if (error || !guide) {\n return (\n <Dialog open={true} onOpenChange={() => onClose()}>\n <DialogContent className=\"max-w-md\">\n <div className=\"text-center py-6\">\n <div className=\"text-gray-400 mb-4\">\n <i className=\"material-icons text-6xl\">error_outline</i>\n </div>\n <h3 className=\"text-lg font-medium text-gray-900 mb-2\">Guide Not Found</h3>\n <p className=\"text-gray-600 mb-4\">The repair guide you're looking for doesn't exist.</p>\n <Button onClick={onClose}>Close</Button>\n </div>\n </DialogContent>\n </Dialog>\n );\n }\n\n const getDifficultyColor = (difficulty: string) => {\n switch (difficulty) {\n case \"easy\":\n return \"bg-success-50 text-success-600\";\n case \"medium\":\n return \"bg-warning-50 text-warning-700\";\n case \"hard\":\n return \"bg-danger-50 text-danger-700\";\n default:\n return \"bg-gray-50 text-gray-600\";\n }\n };\n\n const modalContent = (\n <>\n <DialogHeader className=\"sticky top-0 bg-white border-b border-gray-200 px-6 py-4 rounded-t-xl\">\n <div className=\"flex items-center justify-between\">\n <DialogTitle className=\"text-xl font-medium text-gray-900\">{guide.title}</DialogTitle>\n <div className=\"flex items-center space-x-2\">\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => bookmarkMutation.mutate()}\n disabled={bookmarkMutation.isPending}\n >\n <i className={`material-icons ${guide.isBookmarked ? \"text-primary-700\" : \"text-gray-400\"}`}>\n {guide.isBookmarked ? \"bookmark\" : \"bookmark_border\"}\n </i>\n </Button>\n {!isPage && (\n <Button variant=\"ghost\" size=\"icon\" onClick={onClose}>\n <i className=\"material-icons\">close</i>\n </Button>\n )}\n </div>\n </div>\n <div className=\"flex items-center space-x-4 mt-2\">\n <span className={`inline-block px-2 py-1 text-xs font-medium rounded-full ${getDifficultyColor(guide.difficulty)}`}>\n {guide.difficulty.toUpperCase()}\n </span>\n <span className=\"inline-block px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded-full\">\n {guide.estimatedTime}\n </span>\n <span className=\"text-sm text-gray-500\">Updated recently</span>\n </div>\n </DialogHeader>\n\n <div className=\"p-6\">\n {/* Safety Warning */}\n {guide.safetyWarnings.length > 0 && (\n <div className=\"bg-yellow-50 border-l-4 border-yellow-400 p-4 mb-6\">\n <div className=\"flex items-start space-x-3\">\n <i className=\"material-icons text-yellow-600 text-lg mt-0.5\">warning</i>\n <div>\n <h3 className=\"font-medium text-yellow-800 mb-1\">Safety Precautions</h3>\n <ul className=\"text-sm text-yellow-700 space-y-1\">\n {guide.safetyWarnings.map((warning, index) => (\n <li key={index}>• {warning}</li>\n ))}\n </ul>\n </div>\n </div>\n </div>\n )}\n\n {/* Required Tools */}\n <div className=\"mb-6\">\n <h3 className=\"font-medium text-gray-900 mb-3\">Required Tools</h3>\n <div className=\"grid grid-cols-1 md:grid-cols-3 gap-3\">\n {guide.toolsRequired.map((tool, index) => (\n <div key={index} className=\"flex items-center space-x-3 p-3 bg-gray-50 rounded-lg\">\n <i className=\"material-icons text-gray-600\">build</i>\n <span className=\"text-sm\">{tool}</span>\n </div>\n ))}\n </div>\n </div>\n\n {/* Step-by-step Instructions */}\n <div className=\"space-y-6\">\n <h3 className=\"font-medium text-gray-900\">Installation Steps</h3>\n\n {guide.steps.map((step) => (\n <div key={step.stepNumber} className=\"flex space-x-4\">\n <div className=\"flex-shrink-0 w-8 h-8 bg-primary-700 text-white rounded-full flex items-center justify-center text-sm font-medium\">\n {step.stepNumber}\n </div>\n <div className=\"flex-1\">\n <h4 className=\"font-medium text-gray-900 mb-2\">{step.title}</h4>\n <p className=\"text-sm text-gray-600 mb-3\">{step.description}</p>\n \n {step.imageUrl && (\n <img\n src={step.imageUrl}\n alt={step.title}\n className=\"rounded-lg shadow-sm w-full max-w-md mb-3\"\n />\n )}\n\n {step.notes && step.notes.length > 0 && (\n <div className=\"bg-blue-50 border border-blue-200 rounded-lg p-3 mb-3\">\n <p className=\"text-sm text-blue-800\">\n <strong>Note:</strong> {step.notes.join(\", \")}\n </p>\n </div>\n )}\n\n {step.warnings && step.warnings.length > 0 && (\n <div className=\"bg-red-50 border border-red-200 rounded-lg p-3\">\n <p className=\"text-sm text-red-800\">\n <strong>Warning:</strong> {step.warnings.join(\", \")}\n </p>\n </div>\n )}\n </div>\n </div>\n ))}\n\n {/* Alternative Solutions */}\n {guide.alternativeSolutions && (\n <div className=\"bg-gray-50 rounded-lg p-4\">\n <h4 className=\"font-medium text-gray-900 mb-2\">Alternative Solutions</h4>\n <p className=\"text-sm text-gray-600\">{guide.alternativeSolutions}</p>\n </div>\n )}\n </div>\n </div>\n </>\n );\n\n if (isPage) {\n return (\n <div className=\"max-w-4xl mx-auto px-4 py-8\">\n <div className=\"bg-white rounded-xl shadow-material-lg\">\n {modalContent}\n </div>\n </div>\n );\n }\n\n return (\n <Dialog open={true} onOpenChange={() => onClose()}>\n <DialogContent className=\"max-w-4xl max-h-[90vh] overflow-y-auto p-0\">\n {modalContent}\n </DialogContent>\n </Dialog>\n );\n}\n","size_bytes":7898},"client/src/components/repair/parts-identification.tsx":{"content":"import { useState } from \"react\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport type { DeviceComponent } from \"@shared/schema\";\n\ninterface PartsIdentificationProps {\n deviceType: string;\n}\n\nexport default function PartsIdentification({ deviceType }: PartsIdentificationProps) {\n const [selectedComponent, setSelectedComponent] = useState<string | null>(null);\n\n const { data: components = [] } = useQuery<DeviceComponent[]>({\n queryKey: [\"/api/device-components\", deviceType],\n queryFn: () => {\n const params = new URLSearchParams();\n if (deviceType) params.append('deviceType', deviceType);\n return fetch(`/api/device-components?${params}`).then(res => res.json());\n },\n });\n\n const componentColors = [\n { name: \"RAM/Memory\", color: \"bg-success-600\" },\n { name: \"Storage Drive\", color: \"bg-warning-700\" },\n { name: \"Cooling System\", color: \"bg-cyan-600\" },\n { name: \"Battery\", color: \"bg-purple-600\" },\n { name: \"Motherboard\", color: \"bg-primary-700\" },\n ];\n\n const getComponentColor = (name: string) => {\n const component = componentColors.find(c => name.includes(c.name.split('/')[0]));\n return component?.color || \"bg-gray-600\";\n };\n\n return (\n <div className=\"bg-white dark:bg-gray-800 rounded-xl shadow-material p-6\">\n <h2 className=\"text-lg font-medium text-gray-900 dark:text-white mb-4\">Parts Identification Guide</h2>\n <p className=\"text-sm text-gray-600 dark:text-gray-300 mb-6\">Click on components to learn more about them</p>\n\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-6\">\n <div className=\"relative\">\n <img\n src=\"https://pixabay.com/get/g47f3243ed959354ccf54e02cc790eae822f47a5a3e5222dbc4d9becb7bacd50952bf4d9c647072182a34403929751facc95c030ab418c2af1194144c6770113f_1280.jpg\"\n alt=\"Laptop internal components layout\"\n className=\"w-full rounded-lg\"\n />\n\n {/* Interactive hotspots */}\n <button\n onClick={() => setSelectedComponent(\"ram\")}\n className=\"absolute top-1/4 left-1/3 w-6 h-6 bg-primary-700 rounded-full border-4 border-white shadow-lg hover:scale-110 transition-transform\"\n title=\"RAM/Memory\"\n >\n <span className=\"sr-only\">RAM Module</span>\n </button>\n <button\n onClick={() => setSelectedComponent(\"storage\")}\n className=\"absolute top-1/2 left-1/4 w-6 h-6 bg-primary-700 rounded-full border-4 border-white shadow-lg hover:scale-110 transition-transform\"\n title=\"Storage Drive\"\n >\n <span className=\"sr-only\">Storage Drive</span>\n </button>\n <button\n onClick={() => setSelectedComponent(\"cooling\")}\n className=\"absolute top-1/3 right-1/4 w-6 h-6 bg-primary-700 rounded-full border-4 border-white shadow-lg hover:scale-110 transition-transform\"\n title=\"Cooling System\"\n >\n <span className=\"sr-only\">Cooling Fan</span>\n </button>\n </div>\n\n <div className=\"space-y-4\">\n <div className=\"border border-gray-200 dark:border-gray-600 rounded-lg p-4\">\n <h3 className=\"font-medium text-gray-900 dark:text-white mb-2\">Common Components</h3>\n <div className=\"space-y-2\">\n {components.map((component) => (\n <button\n key={component.id}\n onClick={() => setSelectedComponent(component.id)}\n className={`flex items-center space-x-3 p-2 w-full rounded cursor-pointer transition-colors ${\n selectedComponent === component.id ? \"bg-primary-50\" : \"hover:bg-gray-50\"\n }`}\n >\n <div className={`w-3 h-3 rounded-full ${getComponentColor(component.name)}`}></div>\n <span className=\"text-sm\">{component.name}</span>\n </button>\n ))}\n </div>\n </div>\n\n {selectedComponent && (\n <div className=\"border border-gray-200 rounded-lg p-4\">\n <h4 className=\"font-medium text-gray-900 mb-2\">Component Details</h4>\n {(() => {\n const component = components.find(c => c.id === selectedComponent);\n if (component) {\n return (\n <>\n <p className=\"text-sm text-gray-600 mb-3\">{component.description}</p>\n {component.safetyNotes.length > 0 && (\n <div className=\"bg-yellow-50 border border-yellow-200 rounded-lg p-3\">\n <h5 className=\"font-medium text-yellow-800 mb-1\">Safety Notes</h5>\n <ul className=\"text-sm text-yellow-700 space-y-1\">\n {component.safetyNotes.map((note, index) => (\n <li key={index}>• {note}</li>\n ))}\n </ul>\n </div>\n )}\n </>\n );\n }\n return <p className=\"text-sm text-gray-500\">Select a component to view details.</p>;\n })()}\n </div>\n )}\n\n <div className=\"bg-blue-50 border border-blue-200 rounded-lg p-4\">\n <div className=\"flex items-start space-x-3\">\n <i className=\"material-icons text-primary-700 text-lg mt-0.5\">info</i>\n <div>\n <h4 className=\"font-medium text-primary-900 mb-1\">Pro Tip</h4>\n <p className=\"text-sm text-primary-800\">\n Always power down and disconnect the battery before working on internal components.\n </p>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n );\n}\n","size_bytes":5843},"client/src/components/repair/software-links.tsx":{"content":"interface SoftwareLinksProps {\n deviceType: string;\n}\n\nexport default function SoftwareLinks({ deviceType }: SoftwareLinksProps) {\n const diagnosticSoftware = [\n {\n name: \"HWiNFO64\",\n description: \"Comprehensive hardware analysis and monitoring tool\",\n url: \"https://www.hwinfo.com/\",\n category: \"Hardware Diagnostics\",\n icon: \"monitor_heart\",\n free: true,\n },\n {\n name: \"CrystalDiskInfo\",\n description: \"Hard drive health monitoring and S.M.A.R.T. analysis\",\n url: \"https://crystalmark.info/en/software/crystaldiskinfo/\",\n category: \"Storage Diagnostics\",\n icon: \"storage\",\n free: true,\n },\n {\n name: \"MemTest86\",\n description: \"Memory testing tool for detecting RAM issues\",\n url: \"https://www.memtest86.com/\",\n category: \"Memory Testing\",\n icon: \"memory\",\n free: true,\n },\n {\n name: \"GPU-Z\",\n description: \"Graphics card information and monitoring utility\",\n url: \"https://www.techpowerup.com/gpuz/\",\n category: \"Graphics Diagnostics\",\n icon: \"videogame_asset\",\n free: true,\n },\n ];\n\n const repairSoftware = [\n {\n name: \"Rufus\",\n description: \"Create bootable USB drives for OS installation and recovery\",\n url: \"https://rufus.ie/\",\n category: \"Bootable Media\",\n icon: \"usb\",\n free: true,\n },\n {\n name: \"Clonezilla\",\n description: \"Free disk imaging and cloning solution\",\n url: \"https://clonezilla.org/\",\n category: \"Disk Cloning\",\n icon: \"content_copy\",\n free: true,\n },\n {\n name: \"Malwarebytes\",\n description: \"Anti-malware and virus removal tool\",\n url: \"https://www.malwarebytes.com/\",\n category: \"Security\",\n icon: \"security\",\n free: false,\n },\n {\n name: \"DDU (Display Driver Uninstaller)\",\n description: \"Complete graphics driver removal and cleanup\",\n url: \"https://www.guru3d.com/files-details/display-driver-uninstaller-download.html\",\n category: \"Driver Management\",\n icon: \"cleaning_services\",\n free: true,\n },\n ];\n\n const systemUtilities = [\n {\n name: \"Nirsoft Utilities\",\n description: \"Collection of small utilities for Windows troubleshooting\",\n url: \"https://www.nirsoft.net/\",\n category: \"System Utilities\",\n icon: \"build\",\n free: true,\n },\n {\n name: \"Sysinternals Suite\",\n description: \"Microsoft's advanced system utilities and troubleshooting tools\",\n url: \"https://docs.microsoft.com/en-us/sysinternals/\",\n category: \"Advanced Tools\",\n icon: \"settings\",\n free: true,\n },\n {\n name: \"AIDA64\",\n description: \"System information, diagnostics and benchmarking suite\",\n url: \"https://www.aida64.com/\",\n category: \"System Analysis\",\n icon: \"analytics\",\n free: false,\n },\n {\n name: \"TreeSize\",\n description: \"Disk space analyzer to find large files and folders\",\n url: \"https://www.jam-software.com/treesize/\",\n category: \"Disk Analysis\",\n icon: \"folder\",\n free: true,\n },\n ];\n\n const allSoftware = [...diagnosticSoftware, ...repairSoftware, ...systemUtilities];\n\n const handleDownload = (url: string, name: string) => {\n window.open(url, '_blank', 'noopener,noreferrer');\n };\n\n return (\n <div className=\"bg-white dark:bg-gray-800 rounded-xl shadow-material p-6\">\n <div className=\"flex items-center justify-between mb-6\">\n <div>\n <h2 className=\"text-lg font-medium text-gray-900 dark:text-white mb-2\">Essential Software Tools</h2>\n <p className=\"text-sm text-gray-600 dark:text-gray-300\">Download recommended software for {deviceType} troubleshooting and repair</p>\n </div>\n <div className=\"text-xs text-gray-500 dark:text-gray-400 flex items-center space-x-2\">\n <span className=\"flex items-center\">\n <i className=\"material-icons text-success-600 text-sm mr-1\">check_circle</i>\n Free\n </span>\n <span className=\"flex items-center\">\n <i className=\"material-icons text-warning-700 text-sm mr-1\">paid</i>\n Paid\n </span>\n </div>\n </div>\n\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4\">\n {allSoftware.map((software, index) => (\n <div\n key={index}\n className=\"border border-gray-200 dark:border-gray-600 rounded-lg p-4 hover:shadow-material transition-shadow\"\n >\n <div className=\"flex items-start justify-between mb-3\">\n <div className=\"flex items-center space-x-3\">\n <i className={`material-icons text-primary-700 text-xl`}>{software.icon}</i>\n <div>\n <h3 className=\"font-medium text-gray-900 dark:text-white text-sm\">{software.name}</h3>\n <span className=\"text-xs text-gray-500 dark:text-gray-400\">{software.category}</span>\n </div>\n </div>\n <div className=\"flex items-center space-x-1\">\n {software.free ? (\n <i className=\"material-icons text-success-600 text-sm\" title=\"Free\">check_circle</i>\n ) : (\n <i className=\"material-icons text-warning-700 text-sm\" title=\"Paid\">paid</i>\n )}\n </div>\n </div>\n \n <p className=\"text-xs text-gray-600 dark:text-gray-300 mb-3 line-clamp-2\">\n {software.description}\n </p>\n \n <button\n onClick={() => handleDownload(software.url, software.name)}\n className=\"w-full px-3 py-2 bg-primary-700 text-white text-xs rounded-lg hover:bg-primary-900 transition-colors flex items-center justify-center space-x-2\"\n >\n <i className=\"material-icons text-sm\">download</i>\n <span>Download / Visit</span>\n </button>\n </div>\n ))}\n </div>\n\n <div className=\"mt-6 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4\">\n <div className=\"flex items-start space-x-3\">\n <i className=\"material-icons text-primary-700 text-lg mt-0.5\">info</i>\n <div>\n <h4 className=\"font-medium text-primary-900 dark:text-primary-300 mb-1\">Safety Reminder</h4>\n <p className=\"text-sm text-primary-800 dark:text-primary-200\">\n Always download software from official sources. Scan downloads with antivirus software before installation. \n Create system backups before running diagnostic or repair tools.\n </p>\n </div>\n </div>\n </div>\n </div>\n );\n}","size_bytes":6753},"client/src/components/repair/troubleshooting-flowchart.tsx":{"content":"import { useState } from \"react\";\nimport { useLocation } from \"wouter\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { Button } from \"@/components/ui/button\";\nimport type { TroubleshootingFlow } from \"@shared/schema\";\n\ninterface TroubleshootingFlowchartProps {\n selectedFlow?: TroubleshootingFlow;\n}\n\nexport default function TroubleshootingFlowchart({ selectedFlow }: TroubleshootingFlowchartProps) {\n const [, setLocation] = useLocation();\n const [currentStepId, setCurrentStepId] = useState<string | null>(null);\n const [solution, setSolution] = useState<string | null>(null);\n\n const { data: flows = [] } = useQuery<TroubleshootingFlow[]>({\n queryKey: [\"/api/troubleshooting-flows\"],\n });\n\n const troubleshootingTypes = [\n {\n id: \"power\",\n title: \"Device Won't Turn On\",\n description: \"Power issues, dead battery, charging problems\",\n icon: \"power_off\",\n color: \"text-danger-500\",\n },\n {\n id: \"performance\",\n title: \"Slow Performance\",\n description: \"Freezing, overheating, software issues\",\n icon: \"speed\",\n color: \"text-warning-500\",\n },\n {\n id: \"display\",\n title: \"Display Problems\",\n description: \"Screen flickering, no display, broken LCD\",\n icon: \"monitor\",\n color: \"text-primary\",\n },\n ];\n\n const handleStartTroubleshooting = (type: string) => {\n const flow = flows.find(f => f.type === type);\n if (flow) {\n setCurrentStepId(flow.steps[0]?.id || null);\n setSolution(null);\n setLocation(`/troubleshooting/${type}`);\n }\n };\n\n const handleAnswer = (answer: { text: string; nextStepId?: string; solutionId?: string }) => {\n if (answer.nextStepId) {\n setCurrentStepId(answer.nextStepId);\n } else if (answer.solutionId) {\n setSolution(answer.solutionId);\n setCurrentStepId(null);\n }\n };\n\n const reset = () => {\n setCurrentStepId(null);\n setSolution(null);\n };\n\n if (selectedFlow && (currentStepId || solution)) {\n const currentStep = selectedFlow.steps.find(s => s.id === currentStepId);\n\n return (\n <div className=\"bg-white rounded-xl shadow-material p-6\">\n <div className=\"flex items-center justify-between mb-6\">\n <h2 className=\"text-lg font-medium text-gray-900\">{selectedFlow.title}</h2>\n <Button variant=\"outline\" onClick={reset}>\n Start Over\n </Button>\n </div>\n\n {solution ? (\n <div className=\"text-center py-8\">\n <div className=\"text-green-500 mb-4\">\n <i className=\"material-icons text-6xl\">check_circle</i>\n </div>\n <h3 className=\"text-xl font-medium text-gray-900 mb-2\">Diagnosis Complete</h3>\n <p className=\"text-gray-600 mb-6\">Recommended solution: {solution}</p>\n <div className=\"space-y-2\">\n <Button onClick={reset} className=\"mr-4\">\n Try Again\n </Button>\n <Button variant=\"outline\" onClick={() => setLocation(\"/\")}>\n Browse Repair Guides\n </Button>\n </div>\n </div>\n ) : currentStep ? (\n <div className=\"max-w-2xl mx-auto\">\n <div className=\"mb-6\">\n <h3 className=\"text-lg font-medium text-gray-900 mb-4\">{currentStep.question}</h3>\n <div className=\"space-y-3\">\n {currentStep.answers.map((answer, index) => (\n <Button\n key={index}\n variant=\"outline\"\n onClick={() => handleAnswer(answer)}\n className=\"w-full justify-start p-4 h-auto text-left\"\n >\n {answer.text}\n </Button>\n ))}\n </div>\n </div>\n </div>\n ) : null}\n </div>\n );\n }\n\n return (\n <div className=\"bg-white dark:bg-gray-800 rounded-xl shadow-material p-6\">\n <h2 className=\"text-lg font-medium text-gray-900 dark:text-white mb-4\">Quick Troubleshooting</h2>\n <p className=\"text-sm text-gray-600 dark:text-gray-300 mb-6\">Start here if you're not sure what's wrong with your device</p>\n\n <div className=\"grid grid-cols-1 md:grid-cols-3 gap-4\">\n {troubleshootingTypes.map((type) => (\n <button\n key={type.id}\n onClick={() => handleStartTroubleshooting(type.id)}\n className=\"p-4 border-2 border-gray-200 rounded-lg hover:border-primary hover:bg-primary-50 transition-colors text-left\"\n >\n <div className=\"flex items-center space-x-3 mb-2\">\n <i className={`material-icons ${type.color}`}>{type.icon}</i>\n <h3 className=\"font-medium\">{type.title}</h3>\n </div>\n <p className=\"text-sm text-gray-600\">{type.description}</p>\n </button>\n ))}\n </div>\n </div>\n );\n}\n","size_bytes":4889},"client/src/components/ui/accordion.tsx":{"content":"import * as React from \"react\"\nimport * as AccordionPrimitive from \"@radix-ui/react-accordion\"\nimport { ChevronDown } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Accordion = AccordionPrimitive.Root\n\nconst AccordionItem = React.forwardRef<\n React.ElementRef<typeof AccordionPrimitive.Item>,\n React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>\n>(({ className, ...props }, ref) => (\n <AccordionPrimitive.Item\n ref={ref}\n className={cn(\"border-b\", className)}\n {...props}\n />\n))\nAccordionItem.displayName = \"AccordionItem\"\n\nconst AccordionTrigger = React.forwardRef<\n React.ElementRef<typeof AccordionPrimitive.Trigger>,\n React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>\n>(({ className, children, ...props }, ref) => (\n <AccordionPrimitive.Header className=\"flex\">\n <AccordionPrimitive.Trigger\n ref={ref}\n className={cn(\n \"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180\",\n className\n )}\n {...props}\n >\n {children}\n <ChevronDown className=\"h-4 w-4 shrink-0 transition-transform duration-200\" />\n </AccordionPrimitive.Trigger>\n </AccordionPrimitive.Header>\n))\nAccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName\n\nconst AccordionContent = React.forwardRef<\n React.ElementRef<typeof AccordionPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n <AccordionPrimitive.Content\n ref={ref}\n className=\"overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down\"\n {...props}\n >\n <div className={cn(\"pb-4 pt-0\", className)}>{children}</div>\n </AccordionPrimitive.Content>\n))\n\nAccordionContent.displayName = AccordionPrimitive.Content.displayName\n\nexport { Accordion, AccordionItem, AccordionTrigger, AccordionContent }\n","size_bytes":1977},"client/src/components/ui/alert-dialog.tsx":{"content":"import * as React from \"react\"\nimport * as AlertDialogPrimitive from \"@radix-ui/react-alert-dialog\"\n\nimport { cn } from \"@/lib/utils\"\nimport { buttonVariants } from \"@/components/ui/button\"\n\nconst AlertDialog = AlertDialogPrimitive.Root\n\nconst AlertDialogTrigger = AlertDialogPrimitive.Trigger\n\nconst AlertDialogPortal = AlertDialogPrimitive.Portal\n\nconst AlertDialogOverlay = React.forwardRef<\n React.ElementRef<typeof AlertDialogPrimitive.Overlay>,\n React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n <AlertDialogPrimitive.Overlay\n className={cn(\n \"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\",\n className\n )}\n {...props}\n ref={ref}\n />\n))\nAlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName\n\nconst AlertDialogContent = React.forwardRef<\n React.ElementRef<typeof AlertDialogPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>\n>(({ className, ...props }, ref) => (\n <AlertDialogPortal>\n <AlertDialogOverlay />\n <AlertDialogPrimitive.Content\n ref={ref}\n className={cn(\n \"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\",\n className\n )}\n {...props}\n />\n </AlertDialogPortal>\n))\nAlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName\n\nconst AlertDialogHeader = ({\n className,\n ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n <div\n className={cn(\n \"flex flex-col space-y-2 text-center sm:text-left\",\n className\n )}\n {...props}\n />\n)\nAlertDialogHeader.displayName = \"AlertDialogHeader\"\n\nconst AlertDialogFooter = ({\n className,\n ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n <div\n className={cn(\n \"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2\",\n className\n )}\n {...props}\n />\n)\nAlertDialogFooter.displayName = \"AlertDialogFooter\"\n\nconst AlertDialogTitle = React.forwardRef<\n React.ElementRef<typeof AlertDialogPrimitive.Title>,\n React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n <AlertDialogPrimitive.Title\n ref={ref}\n className={cn(\"text-lg font-semibold\", className)}\n {...props}\n />\n))\nAlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName\n\nconst AlertDialogDescription = React.forwardRef<\n React.ElementRef<typeof AlertDialogPrimitive.Description>,\n React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n <AlertDialogPrimitive.Description\n ref={ref}\n className={cn(\"text-sm text-muted-foreground\", className)}\n {...props}\n />\n))\nAlertDialogDescription.displayName =\n AlertDialogPrimitive.Description.displayName\n\nconst AlertDialogAction = React.forwardRef<\n React.ElementRef<typeof AlertDialogPrimitive.Action>,\n React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>\n>(({ className, ...props }, ref) => (\n <AlertDialogPrimitive.Action\n ref={ref}\n className={cn(buttonVariants(), className)}\n {...props}\n />\n))\nAlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName\n\nconst AlertDialogCancel = React.forwardRef<\n React.ElementRef<typeof AlertDialogPrimitive.Cancel>,\n React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>\n>(({ className, ...props }, ref) => (\n <AlertDialogPrimitive.Cancel\n ref={ref}\n className={cn(\n buttonVariants({ variant: \"outline\" }),\n \"mt-2 sm:mt-0\",\n className\n )}\n {...props}\n />\n))\nAlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName\n\nexport {\n AlertDialog,\n AlertDialogPortal,\n AlertDialogOverlay,\n AlertDialogTrigger,\n AlertDialogContent,\n AlertDialogHeader,\n AlertDialogFooter,\n AlertDialogTitle,\n AlertDialogDescription,\n AlertDialogAction,\n AlertDialogCancel,\n}\n","size_bytes":4420},"client/src/components/ui/alert.tsx":{"content":"import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst alertVariants = cva(\n \"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground\",\n {\n variants: {\n variant: {\n default: \"bg-background text-foreground\",\n destructive:\n \"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n }\n)\n\nconst Alert = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>\n>(({ className, variant, ...props }, ref) => (\n <div\n ref={ref}\n role=\"alert\"\n className={cn(alertVariants({ variant }), className)}\n {...props}\n />\n))\nAlert.displayName = \"Alert\"\n\nconst AlertTitle = React.forwardRef<\n HTMLParagraphElement,\n React.HTMLAttributes<HTMLHeadingElement>\n>(({ className, ...props }, ref) => (\n <h5\n ref={ref}\n className={cn(\"mb-1 font-medium leading-none tracking-tight\", className)}\n {...props}\n />\n))\nAlertTitle.displayName = \"AlertTitle\"\n\nconst AlertDescription = React.forwardRef<\n HTMLParagraphElement,\n React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn(\"text-sm [&_p]:leading-relaxed\", className)}\n {...props}\n />\n))\nAlertDescription.displayName = \"AlertDescription\"\n\nexport { Alert, AlertTitle, AlertDescription }\n","size_bytes":1584},"client/src/components/ui/aspect-ratio.tsx":{"content":"import * as AspectRatioPrimitive from \"@radix-ui/react-aspect-ratio\"\n\nconst AspectRatio = AspectRatioPrimitive.Root\n\nexport { AspectRatio }\n","size_bytes":140},"client/src/components/ui/avatar.tsx":{"content":"\"use client\"\n\nimport * as React from \"react\"\nimport * as AvatarPrimitive from \"@radix-ui/react-avatar\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Avatar = React.forwardRef<\n React.ElementRef<typeof AvatarPrimitive.Root>,\n React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>\n>(({ className, ...props }, ref) => (\n <AvatarPrimitive.Root\n ref={ref}\n className={cn(\n \"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full\",\n className\n )}\n {...props}\n />\n))\nAvatar.displayName = AvatarPrimitive.Root.displayName\n\nconst AvatarImage = React.forwardRef<\n React.ElementRef<typeof AvatarPrimitive.Image>,\n React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>\n>(({ className, ...props }, ref) => (\n <AvatarPrimitive.Image\n ref={ref}\n className={cn(\"aspect-square h-full w-full\", className)}\n {...props}\n />\n))\nAvatarImage.displayName = AvatarPrimitive.Image.displayName\n\nconst AvatarFallback = React.forwardRef<\n React.ElementRef<typeof AvatarPrimitive.Fallback>,\n React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>\n>(({ className, ...props }, ref) => (\n <AvatarPrimitive.Fallback\n ref={ref}\n className={cn(\n \"flex h-full w-full items-center justify-center rounded-full bg-muted\",\n className\n )}\n {...props}\n />\n))\nAvatarFallback.displayName = AvatarPrimitive.Fallback.displayName\n\nexport { Avatar, AvatarImage, AvatarFallback }\n","size_bytes":1419},"client/src/components/ui/badge.tsx":{"content":"import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst badgeVariants = cva(\n \"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2\",\n {\n variants: {\n variant: {\n default:\n \"border-transparent bg-primary text-primary-foreground hover:bg-primary/80\",\n secondary:\n \"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n destructive:\n \"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80\",\n outline: \"text-foreground\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n }\n)\n\nexport interface BadgeProps\n extends React.HTMLAttributes<HTMLDivElement>,\n VariantProps<typeof badgeVariants> {}\n\nfunction Badge({ className, variant, ...props }: BadgeProps) {\n return (\n <div className={cn(badgeVariants({ variant }), className)} {...props} />\n )\n}\n\nexport { Badge, badgeVariants }\n","size_bytes":1128},"client/src/components/ui/breadcrumb.tsx":{"content":"import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { ChevronRight, MoreHorizontal } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Breadcrumb = React.forwardRef<\n HTMLElement,\n React.ComponentPropsWithoutRef<\"nav\"> & {\n separator?: React.ReactNode\n }\n>(({ ...props }, ref) => <nav ref={ref} aria-label=\"breadcrumb\" {...props} />)\nBreadcrumb.displayName = \"Breadcrumb\"\n\nconst BreadcrumbList = React.forwardRef<\n HTMLOListElement,\n React.ComponentPropsWithoutRef<\"ol\">\n>(({ className, ...props }, ref) => (\n <ol\n ref={ref}\n className={cn(\n \"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5\",\n className\n )}\n {...props}\n />\n))\nBreadcrumbList.displayName = \"BreadcrumbList\"\n\nconst BreadcrumbItem = React.forwardRef<\n HTMLLIElement,\n React.ComponentPropsWithoutRef<\"li\">\n>(({ className, ...props }, ref) => (\n <li\n ref={ref}\n className={cn(\"inline-flex items-center gap-1.5\", className)}\n {...props}\n />\n))\nBreadcrumbItem.displayName = \"BreadcrumbItem\"\n\nconst BreadcrumbLink = React.forwardRef<\n HTMLAnchorElement,\n React.ComponentPropsWithoutRef<\"a\"> & {\n asChild?: boolean\n }\n>(({ asChild, className, ...props }, ref) => {\n const Comp = asChild ? Slot : \"a\"\n\n return (\n <Comp\n ref={ref}\n className={cn(\"transition-colors hover:text-foreground\", className)}\n {...props}\n />\n )\n})\nBreadcrumbLink.displayName = \"BreadcrumbLink\"\n\nconst BreadcrumbPage = React.forwardRef<\n HTMLSpanElement,\n React.ComponentPropsWithoutRef<\"span\">\n>(({ className, ...props }, ref) => (\n <span\n ref={ref}\n role=\"link\"\n aria-disabled=\"true\"\n aria-current=\"page\"\n className={cn(\"font-normal text-foreground\", className)}\n {...props}\n />\n))\nBreadcrumbPage.displayName = \"BreadcrumbPage\"\n\nconst BreadcrumbSeparator = ({\n children,\n className,\n ...props\n}: React.ComponentProps<\"li\">) => (\n <li\n role=\"presentation\"\n aria-hidden=\"true\"\n className={cn(\"[&>svg]:w-3.5 [&>svg]:h-3.5\", className)}\n {...props}\n >\n {children ?? <ChevronRight />}\n </li>\n)\nBreadcrumbSeparator.displayName = \"BreadcrumbSeparator\"\n\nconst BreadcrumbEllipsis = ({\n className,\n ...props\n}: React.ComponentProps<\"span\">) => (\n <span\n role=\"presentation\"\n aria-hidden=\"true\"\n className={cn(\"flex h-9 w-9 items-center justify-center\", className)}\n {...props}\n >\n <MoreHorizontal className=\"h-4 w-4\" />\n <span className=\"sr-only\">More</span>\n </span>\n)\nBreadcrumbEllipsis.displayName = \"BreadcrumbElipssis\"\n\nexport {\n Breadcrumb,\n BreadcrumbList,\n BreadcrumbItem,\n BreadcrumbLink,\n BreadcrumbPage,\n BreadcrumbSeparator,\n BreadcrumbEllipsis,\n}\n","size_bytes":2712},"client/src/components/ui/button.tsx":{"content":"import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst buttonVariants = cva(\n \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\",\n {\n variants: {\n variant: {\n default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n destructive:\n \"bg-destructive text-destructive-foreground hover:bg-destructive/90\",\n outline:\n \"border border-input bg-background hover:bg-accent hover:text-accent-foreground\",\n secondary:\n \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n ghost: \"hover:bg-accent hover:text-accent-foreground\",\n link: \"text-primary underline-offset-4 hover:underline\",\n },\n size: {\n default: \"h-10 px-4 py-2\",\n sm: \"h-9 rounded-md px-3\",\n lg: \"h-11 rounded-md px-8\",\n icon: \"h-10 w-10\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n }\n)\n\nexport interface ButtonProps\n extends React.ButtonHTMLAttributes<HTMLButtonElement>,\n VariantProps<typeof buttonVariants> {\n asChild?: boolean\n}\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n ({ className, variant, size, asChild = false, ...props }, ref) => {\n const Comp = asChild ? Slot : \"button\"\n return (\n <Comp\n className={cn(buttonVariants({ variant, size, className }))}\n ref={ref}\n {...props}\n />\n )\n }\n)\nButton.displayName = \"Button\"\n\nexport { Button, buttonVariants }\n","size_bytes":1901},"client/src/components/ui/calendar.tsx":{"content":"import * as React from \"react\"\nimport { ChevronLeft, ChevronRight } from \"lucide-react\"\nimport { DayPicker } from \"react-day-picker\"\n\nimport { cn } from \"@/lib/utils\"\nimport { buttonVariants } from \"@/components/ui/button\"\n\nexport type CalendarProps = React.ComponentProps<typeof DayPicker>\n\nfunction Calendar({\n className,\n classNames,\n showOutsideDays = true,\n ...props\n}: CalendarProps) {\n return (\n <DayPicker\n showOutsideDays={showOutsideDays}\n className={cn(\"p-3\", className)}\n classNames={{\n months: \"flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0\",\n month: \"space-y-4\",\n caption: \"flex justify-center pt-1 relative items-center\",\n caption_label: \"text-sm font-medium\",\n nav: \"space-x-1 flex items-center\",\n nav_button: cn(\n buttonVariants({ variant: \"outline\" }),\n \"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100\"\n ),\n nav_button_previous: \"absolute left-1\",\n nav_button_next: \"absolute right-1\",\n table: \"w-full border-collapse space-y-1\",\n head_row: \"flex\",\n head_cell:\n \"text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]\",\n row: \"flex w-full mt-2\",\n cell: \"h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20\",\n day: cn(\n buttonVariants({ variant: \"ghost\" }),\n \"h-9 w-9 p-0 font-normal aria-selected:opacity-100\"\n ),\n day_range_end: \"day-range-end\",\n day_selected:\n \"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground\",\n day_today: \"bg-accent text-accent-foreground\",\n day_outside:\n \"day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground\",\n day_disabled: \"text-muted-foreground opacity-50\",\n day_range_middle:\n \"aria-selected:bg-accent aria-selected:text-accent-foreground\",\n day_hidden: \"invisible\",\n ...classNames,\n }}\n components={{\n IconLeft: ({ className, ...props }) => (\n <ChevronLeft className={cn(\"h-4 w-4\", className)} {...props} />\n ),\n IconRight: ({ className, ...props }) => (\n <ChevronRight className={cn(\"h-4 w-4\", className)} {...props} />\n ),\n }}\n {...props}\n />\n )\n}\nCalendar.displayName = \"Calendar\"\n\nexport { Calendar }\n","size_bytes":2695},"client/src/components/ui/card.tsx":{"content":"import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Card = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn(\n \"rounded-lg border bg-card text-card-foreground shadow-sm\",\n className\n )}\n {...props}\n />\n))\nCard.displayName = \"Card\"\n\nconst CardHeader = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn(\"flex flex-col space-y-1.5 p-6\", className)}\n {...props}\n />\n))\nCardHeader.displayName = \"CardHeader\"\n\nconst CardTitle = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn(\n \"text-2xl font-semibold leading-none tracking-tight\",\n className\n )}\n {...props}\n />\n))\nCardTitle.displayName = \"CardTitle\"\n\nconst CardDescription = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn(\"text-sm text-muted-foreground\", className)}\n {...props}\n />\n))\nCardDescription.displayName = \"CardDescription\"\n\nconst CardContent = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n <div ref={ref} className={cn(\"p-6 pt-0\", className)} {...props} />\n))\nCardContent.displayName = \"CardContent\"\n\nconst CardFooter = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn(\"flex items-center p-6 pt-0\", className)}\n {...props}\n />\n))\nCardFooter.displayName = \"CardFooter\"\n\nexport { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }\n","size_bytes":1858},"client/src/components/ui/carousel.tsx":{"content":"import * as React from \"react\"\nimport useEmblaCarousel, {\n type UseEmblaCarouselType,\n} from \"embla-carousel-react\"\nimport { ArrowLeft, ArrowRight } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\n\ntype CarouselApi = UseEmblaCarouselType[1]\ntype UseCarouselParameters = Parameters<typeof useEmblaCarousel>\ntype CarouselOptions = UseCarouselParameters[0]\ntype CarouselPlugin = UseCarouselParameters[1]\n\ntype CarouselProps = {\n opts?: CarouselOptions\n plugins?: CarouselPlugin\n orientation?: \"horizontal\" | \"vertical\"\n setApi?: (api: CarouselApi) => void\n}\n\ntype CarouselContextProps = {\n carouselRef: ReturnType<typeof useEmblaCarousel>[0]\n api: ReturnType<typeof useEmblaCarousel>[1]\n scrollPrev: () => void\n scrollNext: () => void\n canScrollPrev: boolean\n canScrollNext: boolean\n} & CarouselProps\n\nconst CarouselContext = React.createContext<CarouselContextProps | null>(null)\n\nfunction useCarousel() {\n const context = React.useContext(CarouselContext)\n\n if (!context) {\n throw new Error(\"useCarousel must be used within a <Carousel />\")\n }\n\n return context\n}\n\nconst Carousel = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement> & CarouselProps\n>(\n (\n {\n orientation = \"horizontal\",\n opts,\n setApi,\n plugins,\n className,\n children,\n ...props\n },\n ref\n ) => {\n const [carouselRef, api] = useEmblaCarousel(\n {\n ...opts,\n axis: orientation === \"horizontal\" ? \"x\" : \"y\",\n },\n plugins\n )\n const [canScrollPrev, setCanScrollPrev] = React.useState(false)\n const [canScrollNext, setCanScrollNext] = React.useState(false)\n\n const onSelect = React.useCallback((api: CarouselApi) => {\n if (!api) {\n return\n }\n\n setCanScrollPrev(api.canScrollPrev())\n setCanScrollNext(api.canScrollNext())\n }, [])\n\n const scrollPrev = React.useCallback(() => {\n api?.scrollPrev()\n }, [api])\n\n const scrollNext = React.useCallback(() => {\n api?.scrollNext()\n }, [api])\n\n const handleKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (event.key === \"ArrowLeft\") {\n event.preventDefault()\n scrollPrev()\n } else if (event.key === \"ArrowRight\") {\n event.preventDefault()\n scrollNext()\n }\n },\n [scrollPrev, scrollNext]\n )\n\n React.useEffect(() => {\n if (!api || !setApi) {\n return\n }\n\n setApi(api)\n }, [api, setApi])\n\n React.useEffect(() => {\n if (!api) {\n return\n }\n\n onSelect(api)\n api.on(\"reInit\", onSelect)\n api.on(\"select\", onSelect)\n\n return () => {\n api?.off(\"select\", onSelect)\n }\n }, [api, onSelect])\n\n return (\n <CarouselContext.Provider\n value={{\n carouselRef,\n api: api,\n opts,\n orientation:\n orientation || (opts?.axis === \"y\" ? \"vertical\" : \"horizontal\"),\n scrollPrev,\n scrollNext,\n canScrollPrev,\n canScrollNext,\n }}\n >\n <div\n ref={ref}\n onKeyDownCapture={handleKeyDown}\n className={cn(\"relative\", className)}\n role=\"region\"\n aria-roledescription=\"carousel\"\n {...props}\n >\n {children}\n </div>\n </CarouselContext.Provider>\n )\n }\n)\nCarousel.displayName = \"Carousel\"\n\nconst CarouselContent = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => {\n const { carouselRef, orientation } = useCarousel()\n\n return (\n <div ref={carouselRef} className=\"overflow-hidden\">\n <div\n ref={ref}\n className={cn(\n \"flex\",\n orientation === \"horizontal\" ? \"-ml-4\" : \"-mt-4 flex-col\",\n className\n )}\n {...props}\n />\n </div>\n )\n})\nCarouselContent.displayName = \"CarouselContent\"\n\nconst CarouselItem = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => {\n const { orientation } = useCarousel()\n\n return (\n <div\n ref={ref}\n role=\"group\"\n aria-roledescription=\"slide\"\n className={cn(\n \"min-w-0 shrink-0 grow-0 basis-full\",\n orientation === \"horizontal\" ? \"pl-4\" : \"pt-4\",\n className\n )}\n {...props}\n />\n )\n})\nCarouselItem.displayName = \"CarouselItem\"\n\nconst CarouselPrevious = React.forwardRef<\n HTMLButtonElement,\n React.ComponentProps<typeof Button>\n>(({ className, variant = \"outline\", size = \"icon\", ...props }, ref) => {\n const { orientation, scrollPrev, canScrollPrev } = useCarousel()\n\n return (\n <Button\n ref={ref}\n variant={variant}\n size={size}\n className={cn(\n \"absolute h-8 w-8 rounded-full\",\n orientation === \"horizontal\"\n ? \"-left-12 top-1/2 -translate-y-1/2\"\n : \"-top-12 left-1/2 -translate-x-1/2 rotate-90\",\n className\n )}\n disabled={!canScrollPrev}\n onClick={scrollPrev}\n {...props}\n >\n <ArrowLeft className=\"h-4 w-4\" />\n <span className=\"sr-only\">Previous slide</span>\n </Button>\n )\n})\nCarouselPrevious.displayName = \"CarouselPrevious\"\n\nconst CarouselNext = React.forwardRef<\n HTMLButtonElement,\n React.ComponentProps<typeof Button>\n>(({ className, variant = \"outline\", size = \"icon\", ...props }, ref) => {\n const { orientation, scrollNext, canScrollNext } = useCarousel()\n\n return (\n <Button\n ref={ref}\n variant={variant}\n size={size}\n className={cn(\n \"absolute h-8 w-8 rounded-full\",\n orientation === \"horizontal\"\n ? \"-right-12 top-1/2 -translate-y-1/2\"\n : \"-bottom-12 left-1/2 -translate-x-1/2 rotate-90\",\n className\n )}\n disabled={!canScrollNext}\n onClick={scrollNext}\n {...props}\n >\n <ArrowRight className=\"h-4 w-4\" />\n <span className=\"sr-only\">Next slide</span>\n </Button>\n )\n})\nCarouselNext.displayName = \"CarouselNext\"\n\nexport {\n type CarouselApi,\n Carousel,\n CarouselContent,\n CarouselItem,\n CarouselPrevious,\n CarouselNext,\n}\n","size_bytes":6210},"client/src/components/ui/chart.tsx":{"content":"\"use client\"\n\nimport * as React from \"react\"\nimport * as RechartsPrimitive from \"recharts\"\n\nimport { cn } from \"@/lib/utils\"\n\n// Format: { THEME_NAME: CSS_SELECTOR }\nconst THEMES = { light: \"\", dark: \".dark\" } as const\n\nexport type ChartConfig = {\n [k in string]: {\n label?: React.ReactNode\n icon?: React.ComponentType\n } & (\n | { color?: string; theme?: never }\n | { color?: never; theme: Record<keyof typeof THEMES, string> }\n )\n}\n\ntype ChartContextProps = {\n config: ChartConfig\n}\n\nconst ChartContext = React.createContext<ChartContextProps | null>(null)\n\nfunction useChart() {\n const context = React.useContext(ChartContext)\n\n if (!context) {\n throw new Error(\"useChart must be used within a <ChartContainer />\")\n }\n\n return context\n}\n\nconst ChartContainer = React.forwardRef<\n HTMLDivElement,\n React.ComponentProps<\"div\"> & {\n config: ChartConfig\n children: React.ComponentProps<\n typeof RechartsPrimitive.ResponsiveContainer\n >[\"children\"]\n }\n>(({ id, className, children, config, ...props }, ref) => {\n const uniqueId = React.useId()\n const chartId = `chart-${id || uniqueId.replace(/:/g, \"\")}`\n\n return (\n <ChartContext.Provider value={{ config }}>\n <div\n data-chart={chartId}\n ref={ref}\n className={cn(\n \"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none\",\n className\n )}\n {...props}\n >\n <ChartStyle id={chartId} config={config} />\n <RechartsPrimitive.ResponsiveContainer>\n {children}\n </RechartsPrimitive.ResponsiveContainer>\n </div>\n </ChartContext.Provider>\n )\n})\nChartContainer.displayName = \"Chart\"\n\nconst ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {\n const colorConfig = Object.entries(config).filter(\n ([, config]) => config.theme || config.color\n )\n\n if (!colorConfig.length) {\n return null\n }\n\n return (\n <style\n dangerouslySetInnerHTML={{\n __html: Object.entries(THEMES)\n .map(\n ([theme, prefix]) => `\n${prefix} [data-chart=${id}] {\n${colorConfig\n .map(([key, itemConfig]) => {\n const color =\n itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||\n itemConfig.color\n return color ? ` --color-${key}: ${color};` : null\n })\n .join(\"\\n\")}\n}\n`\n )\n .join(\"\\n\"),\n }}\n />\n )\n}\n\nconst ChartTooltip = RechartsPrimitive.Tooltip\n\nconst ChartTooltipContent = React.forwardRef<\n HTMLDivElement,\n React.ComponentProps<typeof RechartsPrimitive.Tooltip> &\n React.ComponentProps<\"div\"> & {\n hideLabel?: boolean\n hideIndicator?: boolean\n indicator?: \"line\" | \"dot\" | \"dashed\"\n nameKey?: string\n labelKey?: string\n }\n>(\n (\n {\n active,\n payload,\n className,\n indicator = \"dot\",\n hideLabel = false,\n hideIndicator = false,\n label,\n labelFormatter,\n labelClassName,\n formatter,\n color,\n nameKey,\n labelKey,\n },\n ref\n ) => {\n const { config } = useChart()\n\n const tooltipLabel = React.useMemo(() => {\n if (hideLabel || !payload?.length) {\n return null\n }\n\n const [item] = payload\n const key = `${labelKey || item?.dataKey || item?.name || \"value\"}`\n const itemConfig = getPayloadConfigFromPayload(config, item, key)\n const value =\n !labelKey && typeof label === \"string\"\n ? config[label as keyof typeof config]?.label || label\n : itemConfig?.label\n\n if (labelFormatter) {\n return (\n <div className={cn(\"font-medium\", labelClassName)}>\n {labelFormatter(value, payload)}\n </div>\n )\n }\n\n if (!value) {\n return null\n }\n\n return <div className={cn(\"font-medium\", labelClassName)}>{value}</div>\n }, [\n label,\n labelFormatter,\n payload,\n hideLabel,\n labelClassName,\n config,\n labelKey,\n ])\n\n if (!active || !payload?.length) {\n return null\n }\n\n const nestLabel = payload.length === 1 && indicator !== \"dot\"\n\n return (\n <div\n ref={ref}\n className={cn(\n \"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl\",\n className\n )}\n >\n {!nestLabel ? tooltipLabel : null}\n <div className=\"grid gap-1.5\">\n {payload.map((item, index) => {\n const key = `${nameKey || item.name || item.dataKey || \"value\"}`\n const itemConfig = getPayloadConfigFromPayload(config, item, key)\n const indicatorColor = color || item.payload.fill || item.color\n\n return (\n <div\n key={item.dataKey}\n className={cn(\n \"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground\",\n indicator === \"dot\" && \"items-center\"\n )}\n >\n {formatter && item?.value !== undefined && item.name ? (\n formatter(item.value, item.name, item, index, item.payload)\n ) : (\n <>\n {itemConfig?.icon ? (\n <itemConfig.icon />\n ) : (\n !hideIndicator && (\n <div\n className={cn(\n \"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]\",\n {\n \"h-2.5 w-2.5\": indicator === \"dot\",\n \"w-1\": indicator === \"line\",\n \"w-0 border-[1.5px] border-dashed bg-transparent\":\n indicator === \"dashed\",\n \"my-0.5\": nestLabel && indicator === \"dashed\",\n }\n )}\n style={\n {\n \"--color-bg\": indicatorColor,\n \"--color-border\": indicatorColor,\n } as React.CSSProperties\n }\n />\n )\n )}\n <div\n className={cn(\n \"flex flex-1 justify-between leading-none\",\n nestLabel ? \"items-end\" : \"items-center\"\n )}\n >\n <div className=\"grid gap-1.5\">\n {nestLabel ? tooltipLabel : null}\n <span className=\"text-muted-foreground\">\n {itemConfig?.label || item.name}\n </span>\n </div>\n {item.value && (\n <span className=\"font-mono font-medium tabular-nums text-foreground\">\n {item.value.toLocaleString()}\n </span>\n )}\n </div>\n </>\n )}\n </div>\n )\n })}\n </div>\n </div>\n )\n }\n)\nChartTooltipContent.displayName = \"ChartTooltip\"\n\nconst ChartLegend = RechartsPrimitive.Legend\n\nconst ChartLegendContent = React.forwardRef<\n HTMLDivElement,\n React.ComponentProps<\"div\"> &\n Pick<RechartsPrimitive.LegendProps, \"payload\" | \"verticalAlign\"> & {\n hideIcon?: boolean\n nameKey?: string\n }\n>(\n (\n { className, hideIcon = false, payload, verticalAlign = \"bottom\", nameKey },\n ref\n ) => {\n const { config } = useChart()\n\n if (!payload?.length) {\n return null\n }\n\n return (\n <div\n ref={ref}\n className={cn(\n \"flex items-center justify-center gap-4\",\n verticalAlign === \"top\" ? \"pb-3\" : \"pt-3\",\n className\n )}\n >\n {payload.map((item) => {\n const key = `${nameKey || item.dataKey || \"value\"}`\n const itemConfig = getPayloadConfigFromPayload(config, item, key)\n\n return (\n <div\n key={item.value}\n className={cn(\n \"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground\"\n )}\n >\n {itemConfig?.icon && !hideIcon ? (\n <itemConfig.icon />\n ) : (\n <div\n className=\"h-2 w-2 shrink-0 rounded-[2px]\"\n style={{\n backgroundColor: item.color,\n }}\n />\n )}\n {itemConfig?.label}\n </div>\n )\n })}\n </div>\n )\n }\n)\nChartLegendContent.displayName = \"ChartLegend\"\n\n// Helper to extract item config from a payload.\nfunction getPayloadConfigFromPayload(\n config: ChartConfig,\n payload: unknown,\n key: string\n) {\n if (typeof payload !== \"object\" || payload === null) {\n return undefined\n }\n\n const payloadPayload =\n \"payload\" in payload &&\n typeof payload.payload === \"object\" &&\n payload.payload !== null\n ? payload.payload\n : undefined\n\n let configLabelKey: string = key\n\n if (\n key in payload &&\n typeof payload[key as keyof typeof payload] === \"string\"\n ) {\n configLabelKey = payload[key as keyof typeof payload] as string\n } else if (\n payloadPayload &&\n key in payloadPayload &&\n typeof payloadPayload[key as keyof typeof payloadPayload] === \"string\"\n ) {\n configLabelKey = payloadPayload[\n key as keyof typeof payloadPayload\n ] as string\n }\n\n return configLabelKey in config\n ? config[configLabelKey]\n : config[key as keyof typeof config]\n}\n\nexport {\n ChartContainer,\n ChartTooltip,\n ChartTooltipContent,\n ChartLegend,\n ChartLegendContent,\n ChartStyle,\n}\n","size_bytes":10481},"client/src/components/ui/checkbox.tsx":{"content":"import * as React from \"react\"\nimport * as CheckboxPrimitive from \"@radix-ui/react-checkbox\"\nimport { Check } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Checkbox = React.forwardRef<\n React.ElementRef<typeof CheckboxPrimitive.Root>,\n React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>\n>(({ className, ...props }, ref) => (\n <CheckboxPrimitive.Root\n ref={ref}\n className={cn(\n \"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground\",\n className\n )}\n {...props}\n >\n <CheckboxPrimitive.Indicator\n className={cn(\"flex items-center justify-center text-current\")}\n >\n <Check className=\"h-4 w-4\" />\n </CheckboxPrimitive.Indicator>\n </CheckboxPrimitive.Root>\n))\nCheckbox.displayName = CheckboxPrimitive.Root.displayName\n\nexport { Checkbox }\n","size_bytes":1056},"client/src/components/ui/collapsible.tsx":{"content":"\"use client\"\n\nimport * as CollapsiblePrimitive from \"@radix-ui/react-collapsible\"\n\nconst Collapsible = CollapsiblePrimitive.Root\n\nconst CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger\n\nconst CollapsibleContent = CollapsiblePrimitive.CollapsibleContent\n\nexport { Collapsible, CollapsibleTrigger, CollapsibleContent }\n","size_bytes":329},"client/src/components/ui/command.tsx":{"content":"import * as React from \"react\"\nimport { type DialogProps } from \"@radix-ui/react-dialog\"\nimport { Command as CommandPrimitive } from \"cmdk\"\nimport { Search } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Dialog, DialogContent } from \"@/components/ui/dialog\"\n\nconst Command = React.forwardRef<\n React.ElementRef<typeof CommandPrimitive>,\n React.ComponentPropsWithoutRef<typeof CommandPrimitive>\n>(({ className, ...props }, ref) => (\n <CommandPrimitive\n ref={ref}\n className={cn(\n \"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground\",\n className\n )}\n {...props}\n />\n))\nCommand.displayName = CommandPrimitive.displayName\n\nconst CommandDialog = ({ children, ...props }: DialogProps) => {\n return (\n <Dialog {...props}>\n <DialogContent className=\"overflow-hidden p-0 shadow-lg\">\n <Command className=\"[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5\">\n {children}\n </Command>\n </DialogContent>\n </Dialog>\n )\n}\n\nconst CommandInput = React.forwardRef<\n React.ElementRef<typeof CommandPrimitive.Input>,\n React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>\n>(({ className, ...props }, ref) => (\n <div className=\"flex items-center border-b px-3\" cmdk-input-wrapper=\"\">\n <Search className=\"mr-2 h-4 w-4 shrink-0 opacity-50\" />\n <CommandPrimitive.Input\n ref={ref}\n className={cn(\n \"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50\",\n className\n )}\n {...props}\n />\n </div>\n))\n\nCommandInput.displayName = CommandPrimitive.Input.displayName\n\nconst CommandList = React.forwardRef<\n React.ElementRef<typeof CommandPrimitive.List>,\n React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>\n>(({ className, ...props }, ref) => (\n <CommandPrimitive.List\n ref={ref}\n className={cn(\"max-h-[300px] overflow-y-auto overflow-x-hidden\", className)}\n {...props}\n />\n))\n\nCommandList.displayName = CommandPrimitive.List.displayName\n\nconst CommandEmpty = React.forwardRef<\n React.ElementRef<typeof CommandPrimitive.Empty>,\n React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>\n>((props, ref) => (\n <CommandPrimitive.Empty\n ref={ref}\n className=\"py-6 text-center text-sm\"\n {...props}\n />\n))\n\nCommandEmpty.displayName = CommandPrimitive.Empty.displayName\n\nconst CommandGroup = React.forwardRef<\n React.ElementRef<typeof CommandPrimitive.Group>,\n React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>\n>(({ className, ...props }, ref) => (\n <CommandPrimitive.Group\n ref={ref}\n className={cn(\n \"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground\",\n className\n )}\n {...props}\n />\n))\n\nCommandGroup.displayName = CommandPrimitive.Group.displayName\n\nconst CommandSeparator = React.forwardRef<\n React.ElementRef<typeof CommandPrimitive.Separator>,\n React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n <CommandPrimitive.Separator\n ref={ref}\n className={cn(\"-mx-1 h-px bg-border\", className)}\n {...props}\n />\n))\nCommandSeparator.displayName = CommandPrimitive.Separator.displayName\n\nconst CommandItem = React.forwardRef<\n React.ElementRef<typeof CommandPrimitive.Item>,\n React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>\n>(({ className, ...props }, ref) => (\n <CommandPrimitive.Item\n ref={ref}\n className={cn(\n \"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\",\n className\n )}\n {...props}\n />\n))\n\nCommandItem.displayName = CommandPrimitive.Item.displayName\n\nconst CommandShortcut = ({\n className,\n ...props\n}: React.HTMLAttributes<HTMLSpanElement>) => {\n return (\n <span\n className={cn(\n \"ml-auto text-xs tracking-widest text-muted-foreground\",\n className\n )}\n {...props}\n />\n )\n}\nCommandShortcut.displayName = \"CommandShortcut\"\n\nexport {\n Command,\n CommandDialog,\n CommandInput,\n CommandList,\n CommandEmpty,\n CommandGroup,\n CommandItem,\n CommandShortcut,\n CommandSeparator,\n}\n","size_bytes":4885},"client/src/components/ui/context-menu.tsx":{"content":"import * as React from \"react\"\nimport * as ContextMenuPrimitive from \"@radix-ui/react-context-menu\"\nimport { Check, ChevronRight, Circle } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst ContextMenu = ContextMenuPrimitive.Root\n\nconst ContextMenuTrigger = ContextMenuPrimitive.Trigger\n\nconst ContextMenuGroup = ContextMenuPrimitive.Group\n\nconst ContextMenuPortal = ContextMenuPrimitive.Portal\n\nconst ContextMenuSub = ContextMenuPrimitive.Sub\n\nconst ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup\n\nconst ContextMenuSubTrigger = React.forwardRef<\n React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,\n React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {\n inset?: boolean\n }\n>(({ className, inset, children, ...props }, ref) => (\n <ContextMenuPrimitive.SubTrigger\n ref={ref}\n className={cn(\n \"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground\",\n inset && \"pl-8\",\n className\n )}\n {...props}\n >\n {children}\n <ChevronRight className=\"ml-auto h-4 w-4\" />\n </ContextMenuPrimitive.SubTrigger>\n))\nContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName\n\nconst ContextMenuSubContent = React.forwardRef<\n React.ElementRef<typeof ContextMenuPrimitive.SubContent>,\n React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>\n>(({ className, ...props }, ref) => (\n <ContextMenuPrimitive.SubContent\n ref={ref}\n className={cn(\n \"z-50 min-w-[8rem] overflow-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-context-menu-content-transform-origin]\",\n className\n )}\n {...props}\n />\n))\nContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName\n\nconst ContextMenuContent = React.forwardRef<\n React.ElementRef<typeof ContextMenuPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>\n>(({ className, ...props }, ref) => (\n <ContextMenuPrimitive.Portal>\n <ContextMenuPrimitive.Content\n ref={ref}\n className={cn(\n \"z-50 max-h-[--radix-context-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 animate-in fade-in-80 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-context-menu-content-transform-origin]\",\n className\n )}\n {...props}\n />\n </ContextMenuPrimitive.Portal>\n))\nContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName\n\nconst ContextMenuItem = React.forwardRef<\n React.ElementRef<typeof ContextMenuPrimitive.Item>,\n React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {\n inset?: boolean\n }\n>(({ className, inset, ...props }, ref) => (\n <ContextMenuPrimitive.Item\n ref={ref}\n className={cn(\n \"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n inset && \"pl-8\",\n className\n )}\n {...props}\n />\n))\nContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName\n\nconst ContextMenuCheckboxItem = React.forwardRef<\n React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,\n React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>\n>(({ className, children, checked, ...props }, ref) => (\n <ContextMenuPrimitive.CheckboxItem\n ref={ref}\n className={cn(\n \"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n className\n )}\n checked={checked}\n {...props}\n >\n <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n <ContextMenuPrimitive.ItemIndicator>\n <Check className=\"h-4 w-4\" />\n </ContextMenuPrimitive.ItemIndicator>\n </span>\n {children}\n </ContextMenuPrimitive.CheckboxItem>\n))\nContextMenuCheckboxItem.displayName =\n ContextMenuPrimitive.CheckboxItem.displayName\n\nconst ContextMenuRadioItem = React.forwardRef<\n React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,\n React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>\n>(({ className, children, ...props }, ref) => (\n <ContextMenuPrimitive.RadioItem\n ref={ref}\n className={cn(\n \"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n className\n )}\n {...props}\n >\n <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n <ContextMenuPrimitive.ItemIndicator>\n <Circle className=\"h-2 w-2 fill-current\" />\n </ContextMenuPrimitive.ItemIndicator>\n </span>\n {children}\n </ContextMenuPrimitive.RadioItem>\n))\nContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName\n\nconst ContextMenuLabel = React.forwardRef<\n React.ElementRef<typeof ContextMenuPrimitive.Label>,\n React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {\n inset?: boolean\n }\n>(({ className, inset, ...props }, ref) => (\n <ContextMenuPrimitive.Label\n ref={ref}\n className={cn(\n \"px-2 py-1.5 text-sm font-semibold text-foreground\",\n inset && \"pl-8\",\n className\n )}\n {...props}\n />\n))\nContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName\n\nconst ContextMenuSeparator = React.forwardRef<\n React.ElementRef<typeof ContextMenuPrimitive.Separator>,\n React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n <ContextMenuPrimitive.Separator\n ref={ref}\n className={cn(\"-mx-1 my-1 h-px bg-border\", className)}\n {...props}\n />\n))\nContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName\n\nconst ContextMenuShortcut = ({\n className,\n ...props\n}: React.HTMLAttributes<HTMLSpanElement>) => {\n return (\n <span\n className={cn(\n \"ml-auto text-xs tracking-widest text-muted-foreground\",\n className\n )}\n {...props}\n />\n )\n}\nContextMenuShortcut.displayName = \"ContextMenuShortcut\"\n\nexport {\n ContextMenu,\n ContextMenuTrigger,\n ContextMenuContent,\n ContextMenuItem,\n ContextMenuCheckboxItem,\n ContextMenuRadioItem,\n ContextMenuLabel,\n ContextMenuSeparator,\n ContextMenuShortcut,\n ContextMenuGroup,\n ContextMenuPortal,\n ContextMenuSub,\n ContextMenuSubContent,\n ContextMenuSubTrigger,\n ContextMenuRadioGroup,\n}\n","size_bytes":7428},"client/src/components/ui/dialog.tsx":{"content":"\"use client\"\n\nimport * as React from \"react\"\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\"\nimport { X } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Dialog = DialogPrimitive.Root\n\nconst DialogTrigger = DialogPrimitive.Trigger\n\nconst DialogPortal = DialogPrimitive.Portal\n\nconst DialogClose = DialogPrimitive.Close\n\nconst DialogOverlay = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Overlay>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Overlay\n ref={ref}\n className={cn(\n \"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\",\n className\n )}\n {...props}\n />\n))\nDialogOverlay.displayName = DialogPrimitive.Overlay.displayName\n\nconst DialogContent = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n <DialogPortal>\n <DialogOverlay />\n <DialogPrimitive.Content\n ref={ref}\n className={cn(\n \"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\",\n className\n )}\n {...props}\n >\n {children}\n <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\">\n <X className=\"h-4 w-4\" />\n <span className=\"sr-only\">Close</span>\n </DialogPrimitive.Close>\n </DialogPrimitive.Content>\n </DialogPortal>\n))\nDialogContent.displayName = DialogPrimitive.Content.displayName\n\nconst DialogHeader = ({\n className,\n ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n <div\n className={cn(\n \"flex flex-col space-y-1.5 text-center sm:text-left\",\n className\n )}\n {...props}\n />\n)\nDialogHeader.displayName = \"DialogHeader\"\n\nconst DialogFooter = ({\n className,\n ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n <div\n className={cn(\n \"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2\",\n className\n )}\n {...props}\n />\n)\nDialogFooter.displayName = \"DialogFooter\"\n\nconst DialogTitle = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Title>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Title\n ref={ref}\n className={cn(\n \"text-lg font-semibold leading-none tracking-tight\",\n className\n )}\n {...props}\n />\n))\nDialogTitle.displayName = DialogPrimitive.Title.displayName\n\nconst DialogDescription = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Description>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Description\n ref={ref}\n className={cn(\"text-sm text-muted-foreground\", className)}\n {...props}\n />\n))\nDialogDescription.displayName = DialogPrimitive.Description.displayName\n\nexport {\n Dialog,\n DialogPortal,\n DialogOverlay,\n DialogClose,\n DialogTrigger,\n DialogContent,\n DialogHeader,\n DialogFooter,\n DialogTitle,\n DialogDescription,\n}\n","size_bytes":3848},"client/src/components/ui/download-modal.tsx":{"content":"import React, { useState } from \"react\";\nimport { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from \"@/components/ui/dialog\";\nimport { Button } from \"@/components/ui/button\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport { Label } from \"@/components/ui/label\";\nimport { RadioGroup, RadioGroupItem } from \"@/components/ui/radio-group\";\nimport { Progress } from \"@/components/ui/progress\";\nimport { useToast } from \"@/hooks/use-toast\";\nimport { downloadGuide, downloadVideo, getEstimatedSize, isDownloadSupported, type DownloadOptions } from \"@/lib/download-utils\";\nimport { Download, FileText, Image, Video, HardDrive } from \"lucide-react\";\nimport type { RepairGuide } from \"@shared/schema\";\n\ninterface DownloadModalProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n guide: RepairGuide;\n videoUrl?: string;\n}\n\nexport function DownloadModal({ open, onOpenChange, guide, videoUrl }: DownloadModalProps) {\n const { toast } = useToast();\n const [options, setOptions] = useState<DownloadOptions>({\n format: 'html',\n includeImages: true,\n includeVideos: false,\n });\n const [isDownloading, setIsDownloading] = useState(false);\n const [downloadProgress, setDownloadProgress] = useState(0);\n\n const handleDownload = async () => {\n if (!isDownloadSupported()) {\n toast({\n title: \"Download Not Supported\",\n description: \"Your browser doesn't support file downloads.\",\n variant: \"destructive\",\n });\n return;\n }\n\n setIsDownloading(true);\n setDownloadProgress(10);\n\n try {\n // Download guide\n const guideSuccess = await downloadGuide(guide, options);\n setDownloadProgress(50);\n\n if (!guideSuccess) {\n throw new Error(\"Failed to download guide\");\n }\n\n // Download video if requested\n if (options.includeVideos && videoUrl) {\n setDownloadProgress(75);\n const videoSuccess = await downloadVideo(videoUrl, `${guide.title}_video`);\n if (!videoSuccess) {\n toast({\n title: \"Video Download Failed\",\n description: \"The guide was downloaded, but the video failed to download.\",\n variant: \"destructive\",\n });\n }\n }\n\n setDownloadProgress(100);\n\n toast({\n title: \"Download Complete\",\n description: `${guide.title} has been downloaded successfully.`,\n });\n\n onOpenChange(false);\n } catch (error) {\n console.error(\"Download failed:\", error);\n toast({\n title: \"Download Failed\",\n description: \"There was an error downloading the guide. Please try again.\",\n variant: \"destructive\",\n });\n } finally {\n setIsDownloading(false);\n setDownloadProgress(0);\n }\n };\n\n const estimatedSize = getEstimatedSize(guide, options);\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"sm:max-w-md\">\n <DialogHeader>\n <DialogTitle className=\"flex items-center gap-2\">\n <Download className=\"h-5 w-5\" />\n Download Guide\n </DialogTitle>\n <DialogDescription>\n Download \"{guide.title}\" for offline access\n </DialogDescription>\n </DialogHeader>\n\n <div className=\"space-y-6\">\n {/* Format Selection */}\n <div className=\"space-y-3\">\n <Label className=\"text-sm font-medium\">Download Format</Label>\n <RadioGroup\n value={options.format}\n onValueChange={(value) => setOptions({ ...options, format: value as 'html' | 'json' | 'pdf' })}\n >\n <div className=\"flex items-center space-x-2\">\n <RadioGroupItem value=\"html\" id=\"html\" />\n <Label htmlFor=\"html\" className=\"flex items-center gap-2\">\n <FileText className=\"h-4 w-4\" />\n HTML (Recommended)\n </Label>\n </div>\n <div className=\"flex items-center space-x-2\">\n <RadioGroupItem value=\"json\" id=\"json\" />\n <Label htmlFor=\"json\" className=\"flex items-center gap-2\">\n <FileText className=\"h-4 w-4\" />\n JSON (Raw Data)\n </Label>\n </div>\n <div className=\"flex items-center space-x-2\">\n <RadioGroupItem value=\"pdf\" id=\"pdf\" />\n <Label htmlFor=\"pdf\" className=\"flex items-center gap-2\">\n <FileText className=\"h-4 w-4\" />\n PDF (Print-ready)\n </Label>\n </div>\n </RadioGroup>\n </div>\n\n {/* Include Options */}\n <div className=\"space-y-3\">\n <Label className=\"text-sm font-medium\">Include Content</Label>\n <div className=\"space-y-2\">\n <div className=\"flex items-center space-x-2\">\n <Checkbox\n id=\"include-images\"\n checked={options.includeImages}\n onCheckedChange={(checked) => \n setOptions({ ...options, includeImages: checked as boolean })\n }\n />\n <Label htmlFor=\"include-images\" className=\"flex items-center gap-2\">\n <Image className=\"h-4 w-4\" />\n Include Images\n </Label>\n </div>\n {videoUrl && (\n <div className=\"flex items-center space-x-2\">\n <Checkbox\n id=\"include-videos\"\n checked={options.includeVideos}\n onCheckedChange={(checked) => \n setOptions({ ...options, includeVideos: checked as boolean })\n }\n />\n <Label htmlFor=\"include-videos\" className=\"flex items-center gap-2\">\n <Video className=\"h-4 w-4\" />\n Include Video\n </Label>\n </div>\n )}\n </div>\n </div>\n\n {/* Size Estimate */}\n <div className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n <HardDrive className=\"h-4 w-4\" />\n Estimated size: {estimatedSize}\n </div>\n\n {/* Download Progress */}\n {isDownloading && (\n <div className=\"space-y-2\">\n <div className=\"flex justify-between text-sm\">\n <span>Downloading...</span>\n <span>{downloadProgress}%</span>\n </div>\n <Progress value={downloadProgress} className=\"h-2\" />\n </div>\n )}\n\n {/* Action Buttons */}\n <div className=\"flex justify-end gap-2\">\n <Button\n variant=\"outline\"\n onClick={() => onOpenChange(false)}\n disabled={isDownloading}\n >\n Cancel\n </Button>\n <Button\n onClick={handleDownload}\n disabled={isDownloading}\n className=\"flex items-center gap-2\"\n >\n <Download className=\"h-4 w-4\" />\n {isDownloading ? \"Downloading...\" : \"Download\"}\n </Button>\n </div>\n </div>\n </DialogContent>\n </Dialog>\n );\n}","size_bytes":7305},"client/src/components/ui/drawer.tsx":{"content":"\"use client\"\n\nimport * as React from \"react\"\nimport { Drawer as DrawerPrimitive } from \"vaul\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Drawer = ({\n shouldScaleBackground = true,\n ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (\n <DrawerPrimitive.Root\n shouldScaleBackground={shouldScaleBackground}\n {...props}\n />\n)\nDrawer.displayName = \"Drawer\"\n\nconst DrawerTrigger = DrawerPrimitive.Trigger\n\nconst DrawerPortal = DrawerPrimitive.Portal\n\nconst DrawerClose = DrawerPrimitive.Close\n\nconst DrawerOverlay = React.forwardRef<\n React.ElementRef<typeof DrawerPrimitive.Overlay>,\n React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n <DrawerPrimitive.Overlay\n ref={ref}\n className={cn(\"fixed inset-0 z-50 bg-black/80\", className)}\n {...props}\n />\n))\nDrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName\n\nconst DrawerContent = React.forwardRef<\n React.ElementRef<typeof DrawerPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n <DrawerPortal>\n <DrawerOverlay />\n <DrawerPrimitive.Content\n ref={ref}\n className={cn(\n \"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background\",\n className\n )}\n {...props}\n >\n <div className=\"mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted\" />\n {children}\n </DrawerPrimitive.Content>\n </DrawerPortal>\n))\nDrawerContent.displayName = \"DrawerContent\"\n\nconst DrawerHeader = ({\n className,\n ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n <div\n className={cn(\"grid gap-1.5 p-4 text-center sm:text-left\", className)}\n {...props}\n />\n)\nDrawerHeader.displayName = \"DrawerHeader\"\n\nconst DrawerFooter = ({\n className,\n ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n <div\n className={cn(\"mt-auto flex flex-col gap-2 p-4\", className)}\n {...props}\n />\n)\nDrawerFooter.displayName = \"DrawerFooter\"\n\nconst DrawerTitle = React.forwardRef<\n React.ElementRef<typeof DrawerPrimitive.Title>,\n React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>\n>(({ className, ...props }, ref) => (\n <DrawerPrimitive.Title\n ref={ref}\n className={cn(\n \"text-lg font-semibold leading-none tracking-tight\",\n className\n )}\n {...props}\n />\n))\nDrawerTitle.displayName = DrawerPrimitive.Title.displayName\n\nconst DrawerDescription = React.forwardRef<\n React.ElementRef<typeof DrawerPrimitive.Description>,\n React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>\n>(({ className, ...props }, ref) => (\n <DrawerPrimitive.Description\n ref={ref}\n className={cn(\"text-sm text-muted-foreground\", className)}\n {...props}\n />\n))\nDrawerDescription.displayName = DrawerPrimitive.Description.displayName\n\nexport {\n Drawer,\n DrawerPortal,\n DrawerOverlay,\n DrawerTrigger,\n DrawerClose,\n DrawerContent,\n DrawerHeader,\n DrawerFooter,\n DrawerTitle,\n DrawerDescription,\n}\n","size_bytes":3021},"client/src/components/ui/dropdown-menu.tsx":{"content":"import React from \"react\";\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\";\nimport { Check, ChevronRight, Circle } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\nconst DropdownMenu = DropdownMenuPrimitive.Root;\nconst DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;\nconst DropdownMenuGroup = DropdownMenuPrimitive.Group;\nconst DropdownMenuPortal = DropdownMenuPrimitive.Portal;\nconst DropdownMenuSub = DropdownMenuPrimitive.Sub;\nconst DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;\n\nconst DropdownMenuSubTrigger = React.forwardRef<\n React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,\n React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {\n inset?: boolean;\n }\n>(({ className, inset, children, ...props }, ref) => (\n <DropdownMenuPrimitive.SubTrigger\n ref={ref}\n className={cn(\n \"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent\",\n inset && \"pl-8\",\n className\n )}\n {...props}\n >\n {children}\n <ChevronRight className=\"ml-auto h-4 w-4\" />\n </DropdownMenuPrimitive.SubTrigger>\n));\nDropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;\n\nconst DropdownMenuSubContent = React.forwardRef<\n React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,\n React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>\n>(({ className, ...props }, ref) => (\n <DropdownMenuPrimitive.SubContent\n ref={ref}\n className={cn(\n \"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\",\n className\n )}\n {...props}\n />\n));\nDropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;\n\nconst DropdownMenuContent = React.forwardRef<\n React.ElementRef<typeof DropdownMenuPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>\n>(({ className, sideOffset = 4, ...props }, ref) => (\n <DropdownMenuPrimitive.Portal>\n <DropdownMenuPrimitive.Content\n ref={ref}\n sideOffset={sideOffset}\n className={cn(\n \"z-50 min-w-[8rem] overflow-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\",\n className\n )}\n {...props}\n />\n </DropdownMenuPrimitive.Portal>\n));\nDropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;\n\nconst DropdownMenuItem = React.forwardRef<\n React.ElementRef<typeof DropdownMenuPrimitive.Item>,\n React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {\n inset?: boolean;\n }\n>(({ className, inset, ...props }, ref) => (\n <DropdownMenuPrimitive.Item\n ref={ref}\n className={cn(\n \"relative flex cursor-default select-none items-center 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\",\n inset && \"pl-8\",\n className\n )}\n {...props}\n />\n));\nDropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;\n\nconst DropdownMenuCheckboxItem = React.forwardRef<\n React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,\n React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>\n>(({ className, children, checked, ...props }, ref) => (\n <DropdownMenuPrimitive.CheckboxItem\n ref={ref}\n className={cn(\n \"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\",\n className\n )}\n checked={checked}\n {...props}\n >\n <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n <DropdownMenuPrimitive.ItemIndicator>\n <Check className=\"h-4 w-4\" />\n </DropdownMenuPrimitive.ItemIndicator>\n </span>\n {children}\n </DropdownMenuPrimitive.CheckboxItem>\n));\nDropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;\n\nconst DropdownMenuRadioItem = React.forwardRef<\n React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,\n React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>\n>(({ className, children, ...props }, ref) => (\n <DropdownMenuPrimitive.RadioItem\n ref={ref}\n className={cn(\n \"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\",\n className\n )}\n {...props}\n >\n <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n <DropdownMenuPrimitive.ItemIndicator>\n <Circle className=\"h-2 w-2 fill-current\" />\n </DropdownMenuPrimitive.ItemIndicator>\n </span>\n {children}\n </DropdownMenuPrimitive.RadioItem>\n));\nDropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;\n\nconst DropdownMenuLabel = React.forwardRef<\n React.ElementRef<typeof DropdownMenuPrimitive.Label>,\n React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {\n inset?: boolean;\n }\n>(({ className, inset, ...props }, ref) => (\n <DropdownMenuPrimitive.Label\n ref={ref}\n className={cn(\n \"px-2 py-1.5 text-sm font-semibold\",\n inset && \"pl-8\",\n className\n )}\n {...props}\n />\n));\nDropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;\n\nconst DropdownMenuSeparator = React.forwardRef<\n React.ElementRef<typeof DropdownMenuPrimitive.Separator>,\n React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n <DropdownMenuPrimitive.Separator\n ref={ref}\n className={cn(\"-mx-1 my-1 h-px bg-muted\", className)}\n {...props}\n />\n));\nDropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;\n\nconst DropdownMenuShortcut = ({\n className,\n ...props\n}: React.HTMLAttributes<HTMLSpanElement>) => {\n return (\n <span\n className={cn(\"ml-auto text-xs tracking-widest opacity-60\", className)}\n {...props}\n />\n );\n};\nDropdownMenuShortcut.displayName = \"DropdownMenuShortcut\";\n\nexport {\n DropdownMenu,\n DropdownMenuTrigger,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuCheckboxItem,\n DropdownMenuRadioItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuShortcut,\n DropdownMenuGroup,\n DropdownMenuPortal,\n DropdownMenuSub,\n DropdownMenuSubContent,\n DropdownMenuSubTrigger,\n DropdownMenuRadioGroup,\n};","size_bytes":7310},"client/src/components/ui/form.tsx":{"content":"\"use client\"\n\nimport * as React from \"react\"\nimport * as LabelPrimitive from \"@radix-ui/react-label\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport {\n Controller,\n FormProvider,\n useFormContext,\n type ControllerProps,\n type FieldPath,\n type FieldValues,\n} from \"react-hook-form\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Label } from \"@/components/ui/label\"\n\nconst Form = FormProvider\n\ntype FormFieldContextValue<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>\n> = {\n name: TName\n}\n\nconst FormFieldContext = React.createContext<FormFieldContextValue>(\n {} as FormFieldContextValue\n)\n\nconst FormField = <\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>\n>({\n ...props\n}: ControllerProps<TFieldValues, TName>) => {\n return (\n <FormFieldContext.Provider value={{ name: props.name }}>\n <Controller {...props} />\n </FormFieldContext.Provider>\n )\n}\n\nconst useFormField = () => {\n const fieldContext = React.useContext(FormFieldContext)\n const itemContext = React.useContext(FormItemContext)\n const { getFieldState, formState } = useFormContext()\n\n const fieldState = getFieldState(fieldContext.name, formState)\n\n if (!fieldContext) {\n throw new Error(\"useFormField should be used within <FormField>\")\n }\n\n const { id } = itemContext\n\n return {\n id,\n name: fieldContext.name,\n formItemId: `${id}-form-item`,\n formDescriptionId: `${id}-form-item-description`,\n formMessageId: `${id}-form-item-message`,\n ...fieldState,\n }\n}\n\ntype FormItemContextValue = {\n id: string\n}\n\nconst FormItemContext = React.createContext<FormItemContextValue>(\n {} as FormItemContextValue\n)\n\nconst FormItem = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => {\n const id = React.useId()\n\n return (\n <FormItemContext.Provider value={{ id }}>\n <div ref={ref} className={cn(\"space-y-2\", className)} {...props} />\n </FormItemContext.Provider>\n )\n})\nFormItem.displayName = \"FormItem\"\n\nconst FormLabel = React.forwardRef<\n React.ElementRef<typeof LabelPrimitive.Root>,\n React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>\n>(({ className, ...props }, ref) => {\n const { error, formItemId } = useFormField()\n\n return (\n <Label\n ref={ref}\n className={cn(error && \"text-destructive\", className)}\n htmlFor={formItemId}\n {...props}\n />\n )\n})\nFormLabel.displayName = \"FormLabel\"\n\nconst FormControl = React.forwardRef<\n React.ElementRef<typeof Slot>,\n React.ComponentPropsWithoutRef<typeof Slot>\n>(({ ...props }, ref) => {\n const { error, formItemId, formDescriptionId, formMessageId } = useFormField()\n\n return (\n <Slot\n ref={ref}\n id={formItemId}\n aria-describedby={\n !error\n ? `${formDescriptionId}`\n : `${formDescriptionId} ${formMessageId}`\n }\n aria-invalid={!!error}\n {...props}\n />\n )\n})\nFormControl.displayName = \"FormControl\"\n\nconst FormDescription = React.forwardRef<\n HTMLParagraphElement,\n React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => {\n const { formDescriptionId } = useFormField()\n\n return (\n <p\n ref={ref}\n id={formDescriptionId}\n className={cn(\"text-sm text-muted-foreground\", className)}\n {...props}\n />\n )\n})\nFormDescription.displayName = \"FormDescription\"\n\nconst FormMessage = React.forwardRef<\n HTMLParagraphElement,\n React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, children, ...props }, ref) => {\n const { error, formMessageId } = useFormField()\n const body = error ? String(error?.message ?? \"\") : children\n\n if (!body) {\n return null\n }\n\n return (\n <p\n ref={ref}\n id={formMessageId}\n className={cn(\"text-sm font-medium text-destructive\", className)}\n {...props}\n >\n {body}\n </p>\n )\n})\nFormMessage.displayName = \"FormMessage\"\n\nexport {\n useFormField,\n Form,\n FormItem,\n FormLabel,\n FormControl,\n FormDescription,\n FormMessage,\n FormField,\n}\n","size_bytes":4120},"client/src/components/ui/hover-card.tsx":{"content":"\"use client\"\n\nimport * as React from \"react\"\nimport * as HoverCardPrimitive from \"@radix-ui/react-hover-card\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst HoverCard = HoverCardPrimitive.Root\n\nconst HoverCardTrigger = HoverCardPrimitive.Trigger\n\nconst HoverCardContent = React.forwardRef<\n React.ElementRef<typeof HoverCardPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>\n>(({ className, align = \"center\", sideOffset = 4, ...props }, ref) => (\n <HoverCardPrimitive.Content\n ref={ref}\n align={align}\n sideOffset={sideOffset}\n className={cn(\n \"z-50 w-64 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-hover-card-content-transform-origin]\",\n className\n )}\n {...props}\n />\n))\nHoverCardContent.displayName = HoverCardPrimitive.Content.displayName\n\nexport { HoverCard, HoverCardTrigger, HoverCardContent }\n","size_bytes":1251},"client/src/components/ui/input-otp.tsx":{"content":"import * as React from \"react\"\nimport { OTPInput, OTPInputContext } from \"input-otp\"\nimport { Dot } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst InputOTP = React.forwardRef<\n React.ElementRef<typeof OTPInput>,\n React.ComponentPropsWithoutRef<typeof OTPInput>\n>(({ className, containerClassName, ...props }, ref) => (\n <OTPInput\n ref={ref}\n containerClassName={cn(\n \"flex items-center gap-2 has-[:disabled]:opacity-50\",\n containerClassName\n )}\n className={cn(\"disabled:cursor-not-allowed\", className)}\n {...props}\n />\n))\nInputOTP.displayName = \"InputOTP\"\n\nconst InputOTPGroup = React.forwardRef<\n React.ElementRef<\"div\">,\n React.ComponentPropsWithoutRef<\"div\">\n>(({ className, ...props }, ref) => (\n <div ref={ref} className={cn(\"flex items-center\", className)} {...props} />\n))\nInputOTPGroup.displayName = \"InputOTPGroup\"\n\nconst InputOTPSlot = React.forwardRef<\n React.ElementRef<\"div\">,\n React.ComponentPropsWithoutRef<\"div\"> & { index: number }\n>(({ index, className, ...props }, ref) => {\n const inputOTPContext = React.useContext(OTPInputContext)\n const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]\n\n return (\n <div\n ref={ref}\n className={cn(\n \"relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md\",\n isActive && \"z-10 ring-2 ring-ring ring-offset-background\",\n className\n )}\n {...props}\n >\n {char}\n {hasFakeCaret && (\n <div className=\"pointer-events-none absolute inset-0 flex items-center justify-center\">\n <div className=\"h-4 w-px animate-caret-blink bg-foreground duration-1000\" />\n </div>\n )}\n </div>\n )\n})\nInputOTPSlot.displayName = \"InputOTPSlot\"\n\nconst InputOTPSeparator = React.forwardRef<\n React.ElementRef<\"div\">,\n React.ComponentPropsWithoutRef<\"div\">\n>(({ ...props }, ref) => (\n <div ref={ref} role=\"separator\" {...props}>\n <Dot />\n </div>\n))\nInputOTPSeparator.displayName = \"InputOTPSeparator\"\n\nexport { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }\n","size_bytes":2154},"client/src/components/ui/input.tsx":{"content":"import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Input = React.forwardRef<HTMLInputElement, React.ComponentProps<\"input\">>(\n ({ className, type, ...props }, ref) => {\n return (\n <input\n type={type}\n className={cn(\n \"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background 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-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm\",\n className\n )}\n ref={ref}\n {...props}\n />\n )\n }\n)\nInput.displayName = \"Input\"\n\nexport { Input }\n","size_bytes":791},"client/src/components/ui/install-banner.tsx":{"content":"import { useState, useEffect } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardContent } from \"@/components/ui/card\";\nimport { installPWA, isPWAInstalled } from \"@/pwa-utils\";\nimport { X, Download } from \"lucide-react\";\n\nexport function InstallBanner() {\n const [showBanner, setShowBanner] = useState(false);\n const [isInstalled, setIsInstalled] = useState(false);\n\n useEffect(() => {\n // Check if PWA is already installed\n const installed = isPWAInstalled();\n setIsInstalled(installed);\n \n // Show banner if not installed and installation is supported\n if (!installed && 'serviceWorker' in navigator) {\n // Wait a bit before showing the banner\n const timer = setTimeout(() => {\n setShowBanner(true);\n }, 3000);\n \n return () => clearTimeout(timer);\n }\n }, []);\n\n const handleInstall = async () => {\n try {\n await installPWA();\n setShowBanner(false);\n } catch (error) {\n console.error('Installation failed:', error);\n }\n };\n\n const handleDismiss = () => {\n setShowBanner(false);\n // Remember user dismissed the banner\n localStorage.setItem('pwa-banner-dismissed', 'true');\n };\n\n // Don't show if already installed or user dismissed\n if (isInstalled || !showBanner || localStorage.getItem('pwa-banner-dismissed')) {\n return null;\n }\n\n return (\n <div className=\"fixed bottom-4 left-4 right-4 z-50 md:left-auto md:right-4 md:w-96\">\n <Card className=\"bg-primary-700 text-white border-primary-600\">\n <CardContent className=\"p-4\">\n <div className=\"flex items-start space-x-3\">\n <div className=\"flex-shrink-0\">\n <Download className=\"h-6 w-6\" />\n </div>\n <div className=\"flex-1 min-w-0\">\n <h3 className=\"text-sm font-medium\">Install JCR Guide Pro</h3>\n <p className=\"text-xs text-primary-100 mt-1\">\n Get instant access to repair guides offline. Install our app for the best experience.\n </p>\n <div className=\"mt-3 flex space-x-2\">\n <Button\n onClick={handleInstall}\n size=\"sm\"\n variant=\"secondary\"\n className=\"bg-white text-primary-700 hover:bg-gray-100\"\n >\n Install\n </Button>\n <Button\n onClick={handleDismiss}\n size=\"sm\"\n variant=\"ghost\"\n className=\"text-primary-100 hover:text-white hover:bg-primary-600\"\n >\n Maybe Later\n </Button>\n </div>\n </div>\n <button\n onClick={handleDismiss}\n className=\"flex-shrink-0 text-primary-200 hover:text-white\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n </div>\n </CardContent>\n </Card>\n </div>\n );\n}","size_bytes":2976},"client/src/components/ui/label.tsx":{"content":"import * as React from \"react\"\nimport * as LabelPrimitive from \"@radix-ui/react-label\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst labelVariants = cva(\n \"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\"\n)\n\nconst Label = React.forwardRef<\n React.ElementRef<typeof LabelPrimitive.Root>,\n React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &\n VariantProps<typeof labelVariants>\n>(({ className, ...props }, ref) => (\n <LabelPrimitive.Root\n ref={ref}\n className={cn(labelVariants(), className)}\n {...props}\n />\n))\nLabel.displayName = LabelPrimitive.Root.displayName\n\nexport { Label }\n","size_bytes":710},"client/src/components/ui/loading-spinner.tsx":{"content":"import { cn } from \"@/lib/utils\";\n\ninterface LoadingSpinnerProps {\n className?: string;\n size?: \"sm\" | \"md\" | \"lg\";\n}\n\nexport function LoadingSpinner({ className, size = \"md\" }: LoadingSpinnerProps) {\n const sizeClasses = {\n sm: \"h-4 w-4\",\n md: \"h-8 w-8\", \n lg: \"h-12 w-12\"\n };\n\n return (\n <div \n className={cn(\n \"animate-spin rounded-full border-4 border-solid border-primary-600 border-r-transparent\",\n sizeClasses[size],\n className\n )}\n />\n );\n}\n\nexport function PageLoadingSpinner() {\n return (\n <div className=\"min-h-screen bg-gray-50 dark:bg-gray-900 amoled:bg-black flex items-center justify-center\">\n <div className=\"text-center\">\n <LoadingSpinner size=\"lg\" className=\"mx-auto mb-4\" />\n <p className=\"text-gray-600 dark:text-gray-300 amoled:text-gray-300\">Loading...</p>\n </div>\n </div>\n );\n}","size_bytes":884},"client/src/components/ui/menubar.tsx":{"content":"\"use client\"\n\nimport * as React from \"react\"\nimport * as MenubarPrimitive from \"@radix-ui/react-menubar\"\nimport { Check, ChevronRight, Circle } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction MenubarMenu({\n ...props\n}: React.ComponentProps<typeof MenubarPrimitive.Menu>) {\n return <MenubarPrimitive.Menu {...props} />\n}\n\nfunction MenubarGroup({\n ...props\n}: React.ComponentProps<typeof MenubarPrimitive.Group>) {\n return <MenubarPrimitive.Group {...props} />\n}\n\nfunction MenubarPortal({\n ...props\n}: React.ComponentProps<typeof MenubarPrimitive.Portal>) {\n return <MenubarPrimitive.Portal {...props} />\n}\n\nfunction MenubarRadioGroup({\n ...props\n}: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {\n return <MenubarPrimitive.RadioGroup {...props} />\n}\n\nfunction MenubarSub({\n ...props\n}: React.ComponentProps<typeof MenubarPrimitive.Sub>) {\n return <MenubarPrimitive.Sub data-slot=\"menubar-sub\" {...props} />\n}\n\nconst Menubar = React.forwardRef<\n React.ElementRef<typeof MenubarPrimitive.Root>,\n React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>\n>(({ className, ...props }, ref) => (\n <MenubarPrimitive.Root\n ref={ref}\n className={cn(\n \"flex h-10 items-center space-x-1 rounded-md border bg-background p-1\",\n className\n )}\n {...props}\n />\n))\nMenubar.displayName = MenubarPrimitive.Root.displayName\n\nconst MenubarTrigger = React.forwardRef<\n React.ElementRef<typeof MenubarPrimitive.Trigger>,\n React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>\n>(({ className, ...props }, ref) => (\n <MenubarPrimitive.Trigger\n ref={ref}\n className={cn(\n \"flex cursor-default select-none items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground\",\n className\n )}\n {...props}\n />\n))\nMenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName\n\nconst MenubarSubTrigger = React.forwardRef<\n React.ElementRef<typeof MenubarPrimitive.SubTrigger>,\n React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {\n inset?: boolean\n }\n>(({ className, inset, children, ...props }, ref) => (\n <MenubarPrimitive.SubTrigger\n ref={ref}\n className={cn(\n \"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground\",\n inset && \"pl-8\",\n className\n )}\n {...props}\n >\n {children}\n <ChevronRight className=\"ml-auto h-4 w-4\" />\n </MenubarPrimitive.SubTrigger>\n))\nMenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName\n\nconst MenubarSubContent = React.forwardRef<\n React.ElementRef<typeof MenubarPrimitive.SubContent>,\n React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>\n>(({ className, ...props }, ref) => (\n <MenubarPrimitive.SubContent\n ref={ref}\n className={cn(\n \"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground 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-menubar-content-transform-origin]\",\n className\n )}\n {...props}\n />\n))\nMenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName\n\nconst MenubarContent = React.forwardRef<\n React.ElementRef<typeof MenubarPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>\n>(\n (\n { className, align = \"start\", alignOffset = -4, sideOffset = 8, ...props },\n ref\n ) => (\n <MenubarPrimitive.Portal>\n <MenubarPrimitive.Content\n ref={ref}\n align={align}\n alignOffset={alignOffset}\n sideOffset={sideOffset}\n className={cn(\n \"z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in 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-menubar-content-transform-origin]\",\n className\n )}\n {...props}\n />\n </MenubarPrimitive.Portal>\n )\n)\nMenubarContent.displayName = MenubarPrimitive.Content.displayName\n\nconst MenubarItem = React.forwardRef<\n React.ElementRef<typeof MenubarPrimitive.Item>,\n React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {\n inset?: boolean\n }\n>(({ className, inset, ...props }, ref) => (\n <MenubarPrimitive.Item\n ref={ref}\n className={cn(\n \"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n inset && \"pl-8\",\n className\n )}\n {...props}\n />\n))\nMenubarItem.displayName = MenubarPrimitive.Item.displayName\n\nconst MenubarCheckboxItem = React.forwardRef<\n React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,\n React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>\n>(({ className, children, checked, ...props }, ref) => (\n <MenubarPrimitive.CheckboxItem\n ref={ref}\n className={cn(\n \"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n className\n )}\n checked={checked}\n {...props}\n >\n <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n <MenubarPrimitive.ItemIndicator>\n <Check className=\"h-4 w-4\" />\n </MenubarPrimitive.ItemIndicator>\n </span>\n {children}\n </MenubarPrimitive.CheckboxItem>\n))\nMenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName\n\nconst MenubarRadioItem = React.forwardRef<\n React.ElementRef<typeof MenubarPrimitive.RadioItem>,\n React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>\n>(({ className, children, ...props }, ref) => (\n <MenubarPrimitive.RadioItem\n ref={ref}\n className={cn(\n \"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n className\n )}\n {...props}\n >\n <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n <MenubarPrimitive.ItemIndicator>\n <Circle className=\"h-2 w-2 fill-current\" />\n </MenubarPrimitive.ItemIndicator>\n </span>\n {children}\n </MenubarPrimitive.RadioItem>\n))\nMenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName\n\nconst MenubarLabel = React.forwardRef<\n React.ElementRef<typeof MenubarPrimitive.Label>,\n React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {\n inset?: boolean\n }\n>(({ className, inset, ...props }, ref) => (\n <MenubarPrimitive.Label\n ref={ref}\n className={cn(\n \"px-2 py-1.5 text-sm font-semibold\",\n inset && \"pl-8\",\n className\n )}\n {...props}\n />\n))\nMenubarLabel.displayName = MenubarPrimitive.Label.displayName\n\nconst MenubarSeparator = React.forwardRef<\n React.ElementRef<typeof MenubarPrimitive.Separator>,\n React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n <MenubarPrimitive.Separator\n ref={ref}\n className={cn(\"-mx-1 my-1 h-px bg-muted\", className)}\n {...props}\n />\n))\nMenubarSeparator.displayName = MenubarPrimitive.Separator.displayName\n\nconst MenubarShortcut = ({\n className,\n ...props\n}: React.HTMLAttributes<HTMLSpanElement>) => {\n return (\n <span\n className={cn(\n \"ml-auto text-xs tracking-widest text-muted-foreground\",\n className\n )}\n {...props}\n />\n )\n}\nMenubarShortcut.displayname = \"MenubarShortcut\"\n\nexport {\n Menubar,\n MenubarMenu,\n MenubarTrigger,\n MenubarContent,\n MenubarItem,\n MenubarSeparator,\n MenubarLabel,\n MenubarCheckboxItem,\n MenubarRadioGroup,\n MenubarRadioItem,\n MenubarPortal,\n MenubarSubContent,\n MenubarSubTrigger,\n MenubarGroup,\n MenubarSub,\n MenubarShortcut,\n}\n","size_bytes":8605},"client/src/components/ui/navigation-menu.tsx":{"content":"import * as React from \"react\"\nimport * as NavigationMenuPrimitive from \"@radix-ui/react-navigation-menu\"\nimport { cva } from \"class-variance-authority\"\nimport { ChevronDown } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst NavigationMenu = React.forwardRef<\n React.ElementRef<typeof NavigationMenuPrimitive.Root>,\n React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>\n>(({ className, children, ...props }, ref) => (\n <NavigationMenuPrimitive.Root\n ref={ref}\n className={cn(\n \"relative z-10 flex max-w-max flex-1 items-center justify-center\",\n className\n )}\n {...props}\n >\n {children}\n <NavigationMenuViewport />\n </NavigationMenuPrimitive.Root>\n))\nNavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName\n\nconst NavigationMenuList = React.forwardRef<\n React.ElementRef<typeof NavigationMenuPrimitive.List>,\n React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>\n>(({ className, ...props }, ref) => (\n <NavigationMenuPrimitive.List\n ref={ref}\n className={cn(\n \"group flex flex-1 list-none items-center justify-center space-x-1\",\n className\n )}\n {...props}\n />\n))\nNavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName\n\nconst NavigationMenuItem = NavigationMenuPrimitive.Item\n\nconst navigationMenuTriggerStyle = cva(\n \"group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[state=open]:text-accent-foreground data-[state=open]:bg-accent/50 data-[state=open]:hover:bg-accent data-[state=open]:focus:bg-accent\"\n)\n\nconst NavigationMenuTrigger = React.forwardRef<\n React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,\n React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>\n>(({ className, children, ...props }, ref) => (\n <NavigationMenuPrimitive.Trigger\n ref={ref}\n className={cn(navigationMenuTriggerStyle(), \"group\", className)}\n {...props}\n >\n {children}{\" \"}\n <ChevronDown\n className=\"relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180\"\n aria-hidden=\"true\"\n />\n </NavigationMenuPrimitive.Trigger>\n))\nNavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName\n\nconst NavigationMenuContent = React.forwardRef<\n React.ElementRef<typeof NavigationMenuPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>\n>(({ className, ...props }, ref) => (\n <NavigationMenuPrimitive.Content\n ref={ref}\n className={cn(\n \"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto \",\n className\n )}\n {...props}\n />\n))\nNavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName\n\nconst NavigationMenuLink = NavigationMenuPrimitive.Link\n\nconst NavigationMenuViewport = React.forwardRef<\n React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,\n React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>\n>(({ className, ...props }, ref) => (\n <div className={cn(\"absolute left-0 top-full flex justify-center\")}>\n <NavigationMenuPrimitive.Viewport\n className={cn(\n \"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]\",\n className\n )}\n ref={ref}\n {...props}\n />\n </div>\n))\nNavigationMenuViewport.displayName =\n NavigationMenuPrimitive.Viewport.displayName\n\nconst NavigationMenuIndicator = React.forwardRef<\n React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,\n React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>\n>(({ className, ...props }, ref) => (\n <NavigationMenuPrimitive.Indicator\n ref={ref}\n className={cn(\n \"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in\",\n className\n )}\n {...props}\n >\n <div className=\"relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md\" />\n </NavigationMenuPrimitive.Indicator>\n))\nNavigationMenuIndicator.displayName =\n NavigationMenuPrimitive.Indicator.displayName\n\nexport {\n navigationMenuTriggerStyle,\n NavigationMenu,\n NavigationMenuList,\n NavigationMenuItem,\n NavigationMenuContent,\n NavigationMenuTrigger,\n NavigationMenuLink,\n NavigationMenuIndicator,\n NavigationMenuViewport,\n}\n","size_bytes":5128},"client/src/components/ui/pagination.tsx":{"content":"import * as React from \"react\"\nimport { ChevronLeft, ChevronRight, MoreHorizontal } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { ButtonProps, buttonVariants } from \"@/components/ui/button\"\n\nconst Pagination = ({ className, ...props }: React.ComponentProps<\"nav\">) => (\n <nav\n role=\"navigation\"\n aria-label=\"pagination\"\n className={cn(\"mx-auto flex w-full justify-center\", className)}\n {...props}\n />\n)\nPagination.displayName = \"Pagination\"\n\nconst PaginationContent = React.forwardRef<\n HTMLUListElement,\n React.ComponentProps<\"ul\">\n>(({ className, ...props }, ref) => (\n <ul\n ref={ref}\n className={cn(\"flex flex-row items-center gap-1\", className)}\n {...props}\n />\n))\nPaginationContent.displayName = \"PaginationContent\"\n\nconst PaginationItem = React.forwardRef<\n HTMLLIElement,\n React.ComponentProps<\"li\">\n>(({ className, ...props }, ref) => (\n <li ref={ref} className={cn(\"\", className)} {...props} />\n))\nPaginationItem.displayName = \"PaginationItem\"\n\ntype PaginationLinkProps = {\n isActive?: boolean\n} & Pick<ButtonProps, \"size\"> &\n React.ComponentProps<\"a\">\n\nconst PaginationLink = ({\n className,\n isActive,\n size = \"icon\",\n ...props\n}: PaginationLinkProps) => (\n <a\n aria-current={isActive ? \"page\" : undefined}\n className={cn(\n buttonVariants({\n variant: isActive ? \"outline\" : \"ghost\",\n size,\n }),\n className\n )}\n {...props}\n />\n)\nPaginationLink.displayName = \"PaginationLink\"\n\nconst PaginationPrevious = ({\n className,\n ...props\n}: React.ComponentProps<typeof PaginationLink>) => (\n <PaginationLink\n aria-label=\"Go to previous page\"\n size=\"default\"\n className={cn(\"gap-1 pl-2.5\", className)}\n {...props}\n >\n <ChevronLeft className=\"h-4 w-4\" />\n <span>Previous</span>\n </PaginationLink>\n)\nPaginationPrevious.displayName = \"PaginationPrevious\"\n\nconst PaginationNext = ({\n className,\n ...props\n}: React.ComponentProps<typeof PaginationLink>) => (\n <PaginationLink\n aria-label=\"Go to next page\"\n size=\"default\"\n className={cn(\"gap-1 pr-2.5\", className)}\n {...props}\n >\n <span>Next</span>\n <ChevronRight className=\"h-4 w-4\" />\n </PaginationLink>\n)\nPaginationNext.displayName = \"PaginationNext\"\n\nconst PaginationEllipsis = ({\n className,\n ...props\n}: React.ComponentProps<\"span\">) => (\n <span\n aria-hidden\n className={cn(\"flex h-9 w-9 items-center justify-center\", className)}\n {...props}\n >\n <MoreHorizontal className=\"h-4 w-4\" />\n <span className=\"sr-only\">More pages</span>\n </span>\n)\nPaginationEllipsis.displayName = \"PaginationEllipsis\"\n\nexport {\n Pagination,\n PaginationContent,\n PaginationEllipsis,\n PaginationItem,\n PaginationLink,\n PaginationNext,\n PaginationPrevious,\n}\n","size_bytes":2751},"client/src/components/ui/popover.tsx":{"content":"import * as React from \"react\"\nimport * as PopoverPrimitive from \"@radix-ui/react-popover\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Popover = PopoverPrimitive.Root\n\nconst PopoverTrigger = PopoverPrimitive.Trigger\n\nconst PopoverContent = React.forwardRef<\n React.ElementRef<typeof PopoverPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>\n>(({ className, align = \"center\", sideOffset = 4, ...props }, ref) => (\n <PopoverPrimitive.Portal>\n <PopoverPrimitive.Content\n ref={ref}\n align={align}\n sideOffset={sideOffset}\n className={cn(\n \"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]\",\n className\n )}\n {...props}\n />\n </PopoverPrimitive.Portal>\n))\nPopoverContent.displayName = PopoverPrimitive.Content.displayName\n\nexport { Popover, PopoverTrigger, PopoverContent }\n","size_bytes":1280},"client/src/components/ui/progress.tsx":{"content":"import React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface ProgressProps extends React.HTMLAttributes<HTMLDivElement> {\n value?: number;\n max?: number;\n}\n\nconst Progress = React.forwardRef<HTMLDivElement, ProgressProps>(\n ({ className, value = 0, max = 100, ...props }, ref) => (\n <div\n ref={ref}\n className={cn(\n \"relative h-4 w-full overflow-hidden rounded-full bg-secondary\",\n className\n )}\n {...props}\n >\n <div\n className=\"h-full w-full flex-1 bg-primary transition-all\"\n style={{ transform: `translateX(-${100 - (value / max) * 100}%)` }}\n />\n </div>\n )\n);\nProgress.displayName = \"Progress\";\n\nexport { Progress };","size_bytes":709},"client/src/components/ui/radio-group.tsx":{"content":"import React from \"react\";\nimport * as RadioGroupPrimitive from \"@radix-ui/react-radio-group\";\nimport { Circle } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\nconst RadioGroup = React.forwardRef<\n React.ElementRef<typeof RadioGroupPrimitive.Root>,\n React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>\n>(({ className, ...props }, ref) => {\n return (\n <RadioGroupPrimitive.Root\n className={cn(\"grid gap-2\", className)}\n {...props}\n ref={ref}\n />\n );\n});\nRadioGroup.displayName = RadioGroupPrimitive.Root.displayName;\n\nconst RadioGroupItem = React.forwardRef<\n React.ElementRef<typeof RadioGroupPrimitive.Item>,\n React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>\n>(({ className, ...props }, ref) => {\n return (\n <RadioGroupPrimitive.Item\n ref={ref}\n className={cn(\n \"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50\",\n className\n )}\n {...props}\n >\n <RadioGroupPrimitive.Indicator className=\"flex items-center justify-center\">\n <Circle className=\"h-2.5 w-2.5 fill-current text-current\" />\n </RadioGroupPrimitive.Indicator>\n </RadioGroupPrimitive.Item>\n );\n});\nRadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;\n\nexport { RadioGroup, RadioGroupItem };","size_bytes":1471},"client/src/components/ui/resizable.tsx":{"content":"\"use client\"\n\nimport { GripVertical } from \"lucide-react\"\nimport * as ResizablePrimitive from \"react-resizable-panels\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst ResizablePanelGroup = ({\n className,\n ...props\n}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (\n <ResizablePrimitive.PanelGroup\n className={cn(\n \"flex h-full w-full data-[panel-group-direction=vertical]:flex-col\",\n className\n )}\n {...props}\n />\n)\n\nconst ResizablePanel = ResizablePrimitive.Panel\n\nconst ResizableHandle = ({\n withHandle,\n className,\n ...props\n}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {\n withHandle?: boolean\n}) => (\n <ResizablePrimitive.PanelResizeHandle\n className={cn(\n \"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90\",\n className\n )}\n {...props}\n >\n {withHandle && (\n <div className=\"z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border\">\n <GripVertical className=\"h-2.5 w-2.5\" />\n </div>\n )}\n </ResizablePrimitive.PanelResizeHandle>\n)\n\nexport { ResizablePanelGroup, ResizablePanel, ResizableHandle }\n","size_bytes":1723},"client/src/components/ui/scroll-area.tsx":{"content":"import * as React from \"react\"\nimport * as ScrollAreaPrimitive from \"@radix-ui/react-scroll-area\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst ScrollArea = React.forwardRef<\n React.ElementRef<typeof ScrollAreaPrimitive.Root>,\n React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>\n>(({ className, children, ...props }, ref) => (\n <ScrollAreaPrimitive.Root\n ref={ref}\n className={cn(\"relative overflow-hidden\", className)}\n {...props}\n >\n <ScrollAreaPrimitive.Viewport className=\"h-full w-full rounded-[inherit]\">\n {children}\n </ScrollAreaPrimitive.Viewport>\n <ScrollBar />\n <ScrollAreaPrimitive.Corner />\n </ScrollAreaPrimitive.Root>\n))\nScrollArea.displayName = ScrollAreaPrimitive.Root.displayName\n\nconst ScrollBar = React.forwardRef<\n React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,\n React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>\n>(({ className, orientation = \"vertical\", ...props }, ref) => (\n <ScrollAreaPrimitive.ScrollAreaScrollbar\n ref={ref}\n orientation={orientation}\n className={cn(\n \"flex touch-none select-none transition-colors\",\n orientation === \"vertical\" &&\n \"h-full w-2.5 border-l border-l-transparent p-[1px]\",\n orientation === \"horizontal\" &&\n \"h-2.5 flex-col border-t border-t-transparent p-[1px]\",\n className\n )}\n {...props}\n >\n <ScrollAreaPrimitive.ScrollAreaThumb className=\"relative flex-1 rounded-full bg-border\" />\n </ScrollAreaPrimitive.ScrollAreaScrollbar>\n))\nScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName\n\nexport { ScrollArea, ScrollBar }\n","size_bytes":1642},"client/src/components/ui/select.tsx":{"content":"\"use client\"\n\nimport * as React from \"react\"\nimport * as SelectPrimitive from \"@radix-ui/react-select\"\nimport { Check, ChevronDown, ChevronUp } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Select = SelectPrimitive.Root\n\nconst SelectGroup = SelectPrimitive.Group\n\nconst SelectValue = SelectPrimitive.Value\n\nconst SelectTrigger = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Trigger>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>\n>(({ className, children, ...props }, ref) => (\n <SelectPrimitive.Trigger\n ref={ref}\n className={cn(\n \"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1\",\n className\n )}\n {...props}\n >\n {children}\n <SelectPrimitive.Icon asChild>\n <ChevronDown className=\"h-4 w-4 opacity-50\" />\n </SelectPrimitive.Icon>\n </SelectPrimitive.Trigger>\n))\nSelectTrigger.displayName = SelectPrimitive.Trigger.displayName\n\nconst SelectScrollUpButton = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>\n>(({ className, ...props }, ref) => (\n <SelectPrimitive.ScrollUpButton\n ref={ref}\n className={cn(\n \"flex cursor-default items-center justify-center py-1\",\n className\n )}\n {...props}\n >\n <ChevronUp className=\"h-4 w-4\" />\n </SelectPrimitive.ScrollUpButton>\n))\nSelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName\n\nconst SelectScrollDownButton = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>\n>(({ className, ...props }, ref) => (\n <SelectPrimitive.ScrollDownButton\n ref={ref}\n className={cn(\n \"flex cursor-default items-center justify-center py-1\",\n className\n )}\n {...props}\n >\n <ChevronDown className=\"h-4 w-4\" />\n </SelectPrimitive.ScrollDownButton>\n))\nSelectScrollDownButton.displayName =\n SelectPrimitive.ScrollDownButton.displayName\n\nconst SelectContent = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>\n>(({ className, children, position = \"popper\", ...props }, ref) => (\n <SelectPrimitive.Portal>\n <SelectPrimitive.Content\n ref={ref}\n className={cn(\n \"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]\",\n position === \"popper\" &&\n \"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1\",\n className\n )}\n position={position}\n {...props}\n >\n <SelectScrollUpButton />\n <SelectPrimitive.Viewport\n className={cn(\n \"p-1\",\n position === \"popper\" &&\n \"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]\"\n )}\n >\n {children}\n </SelectPrimitive.Viewport>\n <SelectScrollDownButton />\n </SelectPrimitive.Content>\n </SelectPrimitive.Portal>\n))\nSelectContent.displayName = SelectPrimitive.Content.displayName\n\nconst SelectLabel = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Label>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>\n>(({ className, ...props }, ref) => (\n <SelectPrimitive.Label\n ref={ref}\n className={cn(\"py-1.5 pl-8 pr-2 text-sm font-semibold\", className)}\n {...props}\n />\n))\nSelectLabel.displayName = SelectPrimitive.Label.displayName\n\nconst SelectItem = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Item>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>\n>(({ className, children, ...props }, ref) => (\n <SelectPrimitive.Item\n ref={ref}\n className={cn(\n \"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n className\n )}\n {...props}\n >\n <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n <SelectPrimitive.ItemIndicator>\n <Check className=\"h-4 w-4\" />\n </SelectPrimitive.ItemIndicator>\n </span>\n\n <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n </SelectPrimitive.Item>\n))\nSelectItem.displayName = SelectPrimitive.Item.displayName\n\nconst SelectSeparator = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Separator>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n <SelectPrimitive.Separator\n ref={ref}\n className={cn(\"-mx-1 my-1 h-px bg-muted\", className)}\n {...props}\n />\n))\nSelectSeparator.displayName = SelectPrimitive.Separator.displayName\n\nexport {\n Select,\n SelectGroup,\n SelectValue,\n SelectTrigger,\n SelectContent,\n SelectLabel,\n SelectItem,\n SelectSeparator,\n SelectScrollUpButton,\n SelectScrollDownButton,\n}\n","size_bytes":5742},"client/src/components/ui/separator.tsx":{"content":"import React from \"react\";\nimport * as SeparatorPrimitive from \"@radix-ui/react-separator\";\nimport { cn } from \"@/lib/utils\";\n\nconst Separator = React.forwardRef<\n React.ElementRef<typeof SeparatorPrimitive.Root>,\n React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>\n>(\n (\n { className, orientation = \"horizontal\", decorative = true, ...props },\n ref\n ) => (\n <SeparatorPrimitive.Root\n ref={ref}\n decorative={decorative}\n orientation={orientation}\n className={cn(\n \"shrink-0 bg-border\",\n orientation === \"horizontal\" ? \"h-[1px] w-full\" : \"h-full w-[1px]\",\n className\n )}\n {...props}\n />\n )\n);\nSeparator.displayName = SeparatorPrimitive.Root.displayName;\n\nexport { Separator };","size_bytes":755},"client/src/components/ui/sheet.tsx":{"content":"import React from \"react\";\nimport * as SheetPrimitive from \"@radix-ui/react-dialog\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { X } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\nconst Sheet = SheetPrimitive.Root;\nconst SheetTrigger = SheetPrimitive.Trigger;\nconst SheetClose = SheetPrimitive.Close;\nconst SheetPortal = SheetPrimitive.Portal;\n\nconst SheetOverlay = React.forwardRef<\n React.ElementRef<typeof SheetPrimitive.Overlay>,\n React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n <SheetPrimitive.Overlay\n className={cn(\n \"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n className\n )}\n {...props}\n ref={ref}\n />\n));\nSheetOverlay.displayName = SheetPrimitive.Overlay.displayName;\n\nconst sheetVariants = cva(\n \"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500\",\n {\n variants: {\n side: {\n top: \"inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top\",\n bottom:\n \"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom\",\n 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\",\n right:\n \"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\",\n },\n },\n defaultVariants: {\n side: \"right\",\n },\n }\n);\n\ninterface SheetContentProps\n extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,\n VariantProps<typeof sheetVariants> {}\n\nconst SheetContent = React.forwardRef<\n React.ElementRef<typeof SheetPrimitive.Content>,\n SheetContentProps\n>(({ side = \"right\", className, children, ...props }, ref) => (\n <SheetPortal>\n <SheetOverlay />\n <SheetPrimitive.Content\n ref={ref}\n className={cn(sheetVariants({ side }), className)}\n {...props}\n >\n {children}\n <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\">\n <X className=\"h-4 w-4\" />\n <span className=\"sr-only\">Close</span>\n </SheetPrimitive.Close>\n </SheetPrimitive.Content>\n </SheetPortal>\n));\nSheetContent.displayName = SheetPrimitive.Content.displayName;\n\nconst SheetHeader = ({\n className,\n ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n <div\n className={cn(\n \"flex flex-col space-y-2 text-center sm:text-left\",\n className\n )}\n {...props}\n />\n);\nSheetHeader.displayName = \"SheetHeader\";\n\nconst SheetFooter = ({\n className,\n ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n <div\n className={cn(\n \"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2\",\n className\n )}\n {...props}\n />\n);\nSheetFooter.displayName = \"SheetFooter\";\n\nconst SheetTitle = React.forwardRef<\n React.ElementRef<typeof SheetPrimitive.Title>,\n React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>\n>(({ className, ...props }, ref) => (\n <SheetPrimitive.Title\n ref={ref}\n className={cn(\"text-lg font-semibold text-foreground\", className)}\n {...props}\n />\n));\nSheetTitle.displayName = SheetPrimitive.Title.displayName;\n\nconst SheetDescription = React.forwardRef<\n React.ElementRef<typeof SheetPrimitive.Description>,\n React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>\n>(({ className, ...props }, ref) => (\n <SheetPrimitive.Description\n ref={ref}\n className={cn(\"text-sm text-muted-foreground\", className)}\n {...props}\n />\n));\nSheetDescription.displayName = SheetPrimitive.Description.displayName;\n\nexport {\n Sheet,\n SheetPortal,\n SheetOverlay,\n SheetTrigger,\n SheetClose,\n SheetContent,\n SheetHeader,\n SheetFooter,\n SheetTitle,\n SheetDescription,\n};","size_bytes":4300},"client/src/components/ui/sidebar.tsx":{"content":"import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { VariantProps, cva } from \"class-variance-authority\"\nimport { PanelLeft } from \"lucide-react\"\n\nimport { useIsMobile } from \"@/hooks/use-mobile\"\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport { Input } from \"@/components/ui/input\"\nimport { Separator } from \"@/components/ui/separator\"\nimport {\n Sheet,\n SheetContent,\n SheetDescription,\n SheetHeader,\n SheetTitle,\n} from \"@/components/ui/sheet\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\nimport {\n Tooltip,\n TooltipContent,\n TooltipProvider,\n TooltipTrigger,\n} from \"@/components/ui/tooltip\"\n\nconst SIDEBAR_COOKIE_NAME = \"sidebar_state\"\nconst SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7\nconst SIDEBAR_WIDTH = \"16rem\"\nconst SIDEBAR_WIDTH_MOBILE = \"18rem\"\nconst SIDEBAR_WIDTH_ICON = \"3rem\"\nconst SIDEBAR_KEYBOARD_SHORTCUT = \"b\"\n\ntype SidebarContextProps = {\n state: \"expanded\" | \"collapsed\"\n open: boolean\n setOpen: (open: boolean) => void\n openMobile: boolean\n setOpenMobile: (open: boolean) => void\n isMobile: boolean\n toggleSidebar: () => void\n}\n\nconst SidebarContext = React.createContext<SidebarContextProps | null>(null)\n\nfunction useSidebar() {\n const context = React.useContext(SidebarContext)\n if (!context) {\n throw new Error(\"useSidebar must be used within a SidebarProvider.\")\n }\n\n return context\n}\n\nconst SidebarProvider = React.forwardRef<\n HTMLDivElement,\n React.ComponentProps<\"div\"> & {\n defaultOpen?: boolean\n open?: boolean\n onOpenChange?: (open: boolean) => void\n }\n>(\n (\n {\n defaultOpen = true,\n open: openProp,\n onOpenChange: setOpenProp,\n className,\n style,\n children,\n ...props\n },\n ref\n ) => {\n const isMobile = useIsMobile()\n const [openMobile, setOpenMobile] = React.useState(false)\n\n // This is the internal state of the sidebar.\n // We use openProp and setOpenProp for control from outside the component.\n const [_open, _setOpen] = React.useState(defaultOpen)\n const open = openProp ?? _open\n const setOpen = React.useCallback(\n (value: boolean | ((value: boolean) => boolean)) => {\n const openState = typeof value === \"function\" ? value(open) : value\n if (setOpenProp) {\n setOpenProp(openState)\n } else {\n _setOpen(openState)\n }\n\n // This sets the cookie to keep the sidebar state.\n document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`\n },\n [setOpenProp, open]\n )\n\n // Helper to toggle the sidebar.\n const toggleSidebar = React.useCallback(() => {\n return isMobile\n ? setOpenMobile((open) => !open)\n : setOpen((open) => !open)\n }, [isMobile, setOpen, setOpenMobile])\n\n // Adds a keyboard shortcut to toggle the sidebar.\n React.useEffect(() => {\n const handleKeyDown = (event: KeyboardEvent) => {\n if (\n event.key === SIDEBAR_KEYBOARD_SHORTCUT &&\n (event.metaKey || event.ctrlKey)\n ) {\n event.preventDefault()\n toggleSidebar()\n }\n }\n\n window.addEventListener(\"keydown\", handleKeyDown)\n return () => window.removeEventListener(\"keydown\", handleKeyDown)\n }, [toggleSidebar])\n\n // We add a state so that we can do data-state=\"expanded\" or \"collapsed\".\n // This makes it easier to style the sidebar with Tailwind classes.\n const state = open ? \"expanded\" : \"collapsed\"\n\n const contextValue = React.useMemo<SidebarContextProps>(\n () => ({\n state,\n open,\n setOpen,\n isMobile,\n openMobile,\n setOpenMobile,\n toggleSidebar,\n }),\n [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]\n )\n\n return (\n <SidebarContext.Provider value={contextValue}>\n <TooltipProvider delayDuration={0}>\n <div\n style={\n {\n \"--sidebar-width\": SIDEBAR_WIDTH,\n \"--sidebar-width-icon\": SIDEBAR_WIDTH_ICON,\n ...style,\n } as React.CSSProperties\n }\n className={cn(\n \"group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar\",\n className\n )}\n ref={ref}\n {...props}\n >\n {children}\n </div>\n </TooltipProvider>\n </SidebarContext.Provider>\n )\n }\n)\nSidebarProvider.displayName = \"SidebarProvider\"\n\nconst Sidebar = React.forwardRef<\n HTMLDivElement,\n React.ComponentProps<\"div\"> & {\n side?: \"left\" | \"right\"\n variant?: \"sidebar\" | \"floating\" | \"inset\"\n collapsible?: \"offcanvas\" | \"icon\" | \"none\"\n }\n>(\n (\n {\n side = \"left\",\n variant = \"sidebar\",\n collapsible = \"offcanvas\",\n className,\n children,\n ...props\n },\n ref\n ) => {\n const { isMobile, state, openMobile, setOpenMobile } = useSidebar()\n\n if (collapsible === \"none\") {\n return (\n <div\n className={cn(\n \"flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground\",\n className\n )}\n ref={ref}\n {...props}\n >\n {children}\n </div>\n )\n }\n\n if (isMobile) {\n return (\n <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>\n <SheetContent\n data-sidebar=\"sidebar\"\n data-mobile=\"true\"\n className=\"w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden\"\n style={\n {\n \"--sidebar-width\": SIDEBAR_WIDTH_MOBILE,\n } as React.CSSProperties\n }\n side={side}\n >\n <SheetHeader className=\"sr-only\">\n <SheetTitle>Sidebar</SheetTitle>\n <SheetDescription>Displays the mobile sidebar.</SheetDescription>\n </SheetHeader>\n <div className=\"flex h-full w-full flex-col\">{children}</div>\n </SheetContent>\n </Sheet>\n )\n }\n\n return (\n <div\n ref={ref}\n className=\"group peer hidden text-sidebar-foreground md:block\"\n data-state={state}\n data-collapsible={state === \"collapsed\" ? collapsible : \"\"}\n data-variant={variant}\n data-side={side}\n >\n {/* This is what handles the sidebar gap on desktop */}\n <div\n className={cn(\n \"relative w-[--sidebar-width] bg-transparent transition-[width] duration-200 ease-linear\",\n \"group-data-[collapsible=offcanvas]:w-0\",\n \"group-data-[side=right]:rotate-180\",\n variant === \"floating\" || variant === \"inset\"\n ? \"group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]\"\n : \"group-data-[collapsible=icon]:w-[--sidebar-width-icon]\"\n )}\n />\n <div\n className={cn(\n \"fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] duration-200 ease-linear md:flex\",\n side === \"left\"\n ? \"left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]\"\n : \"right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]\",\n // Adjust the padding for floating and inset variants.\n variant === \"floating\" || variant === \"inset\"\n ? \"p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]\"\n : \"group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l\",\n className\n )}\n {...props}\n >\n <div\n data-sidebar=\"sidebar\"\n className=\"flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow\"\n >\n {children}\n </div>\n </div>\n </div>\n )\n }\n)\nSidebar.displayName = \"Sidebar\"\n\nconst SidebarTrigger = React.forwardRef<\n React.ElementRef<typeof Button>,\n React.ComponentProps<typeof Button>\n>(({ className, onClick, ...props }, ref) => {\n const { toggleSidebar } = useSidebar()\n\n return (\n <Button\n ref={ref}\n data-sidebar=\"trigger\"\n variant=\"ghost\"\n size=\"icon\"\n className={cn(\"h-7 w-7\", className)}\n onClick={(event) => {\n onClick?.(event)\n toggleSidebar()\n }}\n {...props}\n >\n <PanelLeft />\n <span className=\"sr-only\">Toggle Sidebar</span>\n </Button>\n )\n})\nSidebarTrigger.displayName = \"SidebarTrigger\"\n\nconst SidebarRail = React.forwardRef<\n HTMLButtonElement,\n React.ComponentProps<\"button\">\n>(({ className, ...props }, ref) => {\n const { toggleSidebar } = useSidebar()\n\n return (\n <button\n ref={ref}\n data-sidebar=\"rail\"\n aria-label=\"Toggle Sidebar\"\n tabIndex={-1}\n onClick={toggleSidebar}\n title=\"Toggle Sidebar\"\n className={cn(\n \"absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex\",\n \"[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize\",\n \"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize\",\n \"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar\",\n \"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2\",\n \"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2\",\n className\n )}\n {...props}\n />\n )\n})\nSidebarRail.displayName = \"SidebarRail\"\n\nconst SidebarInset = React.forwardRef<\n HTMLDivElement,\n React.ComponentProps<\"main\">\n>(({ className, ...props }, ref) => {\n return (\n <main\n ref={ref}\n className={cn(\n \"relative flex w-full flex-1 flex-col bg-background\",\n \"md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow\",\n className\n )}\n {...props}\n />\n )\n})\nSidebarInset.displayName = \"SidebarInset\"\n\nconst SidebarInput = React.forwardRef<\n React.ElementRef<typeof Input>,\n React.ComponentProps<typeof Input>\n>(({ className, ...props }, ref) => {\n return (\n <Input\n ref={ref}\n data-sidebar=\"input\"\n className={cn(\n \"h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring\",\n className\n )}\n {...props}\n />\n )\n})\nSidebarInput.displayName = \"SidebarInput\"\n\nconst SidebarHeader = React.forwardRef<\n HTMLDivElement,\n React.ComponentProps<\"div\">\n>(({ className, ...props }, ref) => {\n return (\n <div\n ref={ref}\n data-sidebar=\"header\"\n className={cn(\"flex flex-col gap-2 p-2\", className)}\n {...props}\n />\n )\n})\nSidebarHeader.displayName = \"SidebarHeader\"\n\nconst SidebarFooter = React.forwardRef<\n HTMLDivElement,\n React.ComponentProps<\"div\">\n>(({ className, ...props }, ref) => {\n return (\n <div\n ref={ref}\n data-sidebar=\"footer\"\n className={cn(\"flex flex-col gap-2 p-2\", className)}\n {...props}\n />\n )\n})\nSidebarFooter.displayName = \"SidebarFooter\"\n\nconst SidebarSeparator = React.forwardRef<\n React.ElementRef<typeof Separator>,\n React.ComponentProps<typeof Separator>\n>(({ className, ...props }, ref) => {\n return (\n <Separator\n ref={ref}\n data-sidebar=\"separator\"\n className={cn(\"mx-2 w-auto bg-sidebar-border\", className)}\n {...props}\n />\n )\n})\nSidebarSeparator.displayName = \"SidebarSeparator\"\n\nconst SidebarContent = React.forwardRef<\n HTMLDivElement,\n React.ComponentProps<\"div\">\n>(({ className, ...props }, ref) => {\n return (\n <div\n ref={ref}\n data-sidebar=\"content\"\n className={cn(\n \"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden\",\n className\n )}\n {...props}\n />\n )\n})\nSidebarContent.displayName = \"SidebarContent\"\n\nconst SidebarGroup = React.forwardRef<\n HTMLDivElement,\n React.ComponentProps<\"div\">\n>(({ className, ...props }, ref) => {\n return (\n <div\n ref={ref}\n data-sidebar=\"group\"\n className={cn(\"relative flex w-full min-w-0 flex-col p-2\", className)}\n {...props}\n />\n )\n})\nSidebarGroup.displayName = \"SidebarGroup\"\n\nconst SidebarGroupLabel = React.forwardRef<\n HTMLDivElement,\n React.ComponentProps<\"div\"> & { asChild?: boolean }\n>(({ className, asChild = false, ...props }, ref) => {\n const Comp = asChild ? Slot : \"div\"\n\n return (\n <Comp\n ref={ref}\n data-sidebar=\"group-label\"\n className={cn(\n \"flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0\",\n \"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0\",\n className\n )}\n {...props}\n />\n )\n})\nSidebarGroupLabel.displayName = \"SidebarGroupLabel\"\n\nconst SidebarGroupAction = React.forwardRef<\n HTMLButtonElement,\n React.ComponentProps<\"button\"> & { asChild?: boolean }\n>(({ className, asChild = false, ...props }, ref) => {\n const Comp = asChild ? Slot : \"button\"\n\n return (\n <Comp\n ref={ref}\n data-sidebar=\"group-action\"\n className={cn(\n \"absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0\",\n // Increases the hit area of the button on mobile.\n \"after:absolute after:-inset-2 after:md:hidden\",\n \"group-data-[collapsible=icon]:hidden\",\n className\n )}\n {...props}\n />\n )\n})\nSidebarGroupAction.displayName = \"SidebarGroupAction\"\n\nconst SidebarGroupContent = React.forwardRef<\n HTMLDivElement,\n React.ComponentProps<\"div\">\n>(({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-sidebar=\"group-content\"\n className={cn(\"w-full text-sm\", className)}\n {...props}\n />\n))\nSidebarGroupContent.displayName = \"SidebarGroupContent\"\n\nconst SidebarMenu = React.forwardRef<\n HTMLUListElement,\n React.ComponentProps<\"ul\">\n>(({ className, ...props }, ref) => (\n <ul\n ref={ref}\n data-sidebar=\"menu\"\n className={cn(\"flex w-full min-w-0 flex-col gap-1\", className)}\n {...props}\n />\n))\nSidebarMenu.displayName = \"SidebarMenu\"\n\nconst SidebarMenuItem = React.forwardRef<\n HTMLLIElement,\n React.ComponentProps<\"li\">\n>(({ className, ...props }, ref) => (\n <li\n ref={ref}\n data-sidebar=\"menu-item\"\n className={cn(\"group/menu-item relative\", className)}\n {...props}\n />\n))\nSidebarMenuItem.displayName = \"SidebarMenuItem\"\n\nconst sidebarMenuButtonVariants = cva(\n \"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0\",\n {\n variants: {\n variant: {\n default: \"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground\",\n outline:\n \"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]\",\n },\n size: {\n default: \"h-8 text-sm\",\n sm: \"h-7 text-xs\",\n lg: \"h-12 text-sm group-data-[collapsible=icon]:!p-0\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n }\n)\n\nconst SidebarMenuButton = React.forwardRef<\n HTMLButtonElement,\n React.ComponentProps<\"button\"> & {\n asChild?: boolean\n isActive?: boolean\n tooltip?: string | React.ComponentProps<typeof TooltipContent>\n } & VariantProps<typeof sidebarMenuButtonVariants>\n>(\n (\n {\n asChild = false,\n isActive = false,\n variant = \"default\",\n size = \"default\",\n tooltip,\n className,\n ...props\n },\n ref\n ) => {\n const Comp = asChild ? Slot : \"button\"\n const { isMobile, state } = useSidebar()\n\n const button = (\n <Comp\n ref={ref}\n data-sidebar=\"menu-button\"\n data-size={size}\n data-active={isActive}\n className={cn(sidebarMenuButtonVariants({ variant, size }), className)}\n {...props}\n />\n )\n\n if (!tooltip) {\n return button\n }\n\n if (typeof tooltip === \"string\") {\n tooltip = {\n children: tooltip,\n }\n }\n\n return (\n <Tooltip>\n <TooltipTrigger asChild>{button}</TooltipTrigger>\n <TooltipContent\n side=\"right\"\n align=\"center\"\n hidden={state !== \"collapsed\" || isMobile}\n {...tooltip}\n />\n </Tooltip>\n )\n }\n)\nSidebarMenuButton.displayName = \"SidebarMenuButton\"\n\nconst SidebarMenuAction = React.forwardRef<\n HTMLButtonElement,\n React.ComponentProps<\"button\"> & {\n asChild?: boolean\n showOnHover?: boolean\n }\n>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {\n const Comp = asChild ? Slot : \"button\"\n\n return (\n <Comp\n ref={ref}\n data-sidebar=\"menu-action\"\n className={cn(\n \"absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0\",\n // Increases the hit area of the button on mobile.\n \"after:absolute after:-inset-2 after:md:hidden\",\n \"peer-data-[size=sm]/menu-button:top-1\",\n \"peer-data-[size=default]/menu-button:top-1.5\",\n \"peer-data-[size=lg]/menu-button:top-2.5\",\n \"group-data-[collapsible=icon]:hidden\",\n showOnHover &&\n \"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0\",\n className\n )}\n {...props}\n />\n )\n})\nSidebarMenuAction.displayName = \"SidebarMenuAction\"\n\nconst SidebarMenuBadge = React.forwardRef<\n HTMLDivElement,\n React.ComponentProps<\"div\">\n>(({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-sidebar=\"menu-badge\"\n className={cn(\n \"pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground\",\n \"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground\",\n \"peer-data-[size=sm]/menu-button:top-1\",\n \"peer-data-[size=default]/menu-button:top-1.5\",\n \"peer-data-[size=lg]/menu-button:top-2.5\",\n \"group-data-[collapsible=icon]:hidden\",\n className\n )}\n {...props}\n />\n))\nSidebarMenuBadge.displayName = \"SidebarMenuBadge\"\n\nconst SidebarMenuSkeleton = React.forwardRef<\n HTMLDivElement,\n React.ComponentProps<\"div\"> & {\n showIcon?: boolean\n }\n>(({ className, showIcon = false, ...props }, ref) => {\n // Random width between 50 to 90%.\n const width = React.useMemo(() => {\n return `${Math.floor(Math.random() * 40) + 50}%`\n }, [])\n\n return (\n <div\n ref={ref}\n data-sidebar=\"menu-skeleton\"\n className={cn(\"flex h-8 items-center gap-2 rounded-md px-2\", className)}\n {...props}\n >\n {showIcon && (\n <Skeleton\n className=\"size-4 rounded-md\"\n data-sidebar=\"menu-skeleton-icon\"\n />\n )}\n <Skeleton\n className=\"h-4 max-w-[--skeleton-width] flex-1\"\n data-sidebar=\"menu-skeleton-text\"\n style={\n {\n \"--skeleton-width\": width,\n } as React.CSSProperties\n }\n />\n </div>\n )\n})\nSidebarMenuSkeleton.displayName = \"SidebarMenuSkeleton\"\n\nconst SidebarMenuSub = React.forwardRef<\n HTMLUListElement,\n React.ComponentProps<\"ul\">\n>(({ className, ...props }, ref) => (\n <ul\n ref={ref}\n data-sidebar=\"menu-sub\"\n className={cn(\n \"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5\",\n \"group-data-[collapsible=icon]:hidden\",\n className\n )}\n {...props}\n />\n))\nSidebarMenuSub.displayName = \"SidebarMenuSub\"\n\nconst SidebarMenuSubItem = React.forwardRef<\n HTMLLIElement,\n React.ComponentProps<\"li\">\n>(({ ...props }, ref) => <li ref={ref} {...props} />)\nSidebarMenuSubItem.displayName = \"SidebarMenuSubItem\"\n\nconst SidebarMenuSubButton = React.forwardRef<\n HTMLAnchorElement,\n React.ComponentProps<\"a\"> & {\n asChild?: boolean\n size?: \"sm\" | \"md\"\n isActive?: boolean\n }\n>(({ asChild = false, size = \"md\", isActive, className, ...props }, ref) => {\n const Comp = asChild ? Slot : \"a\"\n\n return (\n <Comp\n ref={ref}\n data-sidebar=\"menu-sub-button\"\n data-size={size}\n data-active={isActive}\n className={cn(\n \"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground\",\n \"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground\",\n size === \"sm\" && \"text-xs\",\n size === \"md\" && \"text-sm\",\n \"group-data-[collapsible=icon]:hidden\",\n className\n )}\n {...props}\n />\n )\n})\nSidebarMenuSubButton.displayName = \"SidebarMenuSubButton\"\n\nexport {\n Sidebar,\n SidebarContent,\n SidebarFooter,\n SidebarGroup,\n SidebarGroupAction,\n SidebarGroupContent,\n SidebarGroupLabel,\n SidebarHeader,\n SidebarInput,\n SidebarInset,\n SidebarMenu,\n SidebarMenuAction,\n SidebarMenuBadge,\n SidebarMenuButton,\n SidebarMenuItem,\n SidebarMenuSkeleton,\n SidebarMenuSub,\n SidebarMenuSubButton,\n SidebarMenuSubItem,\n SidebarProvider,\n SidebarRail,\n SidebarSeparator,\n SidebarTrigger,\n useSidebar,\n}\n","size_bytes":23567},"client/src/components/ui/skeleton.tsx":{"content":"import { cn } from \"@/lib/utils\"\n\nfunction Skeleton({\n className,\n ...props\n}: React.HTMLAttributes<HTMLDivElement>) {\n return (\n <div\n className={cn(\"animate-pulse rounded-md bg-muted\", className)}\n {...props}\n />\n )\n}\n\nexport { Skeleton }\n","size_bytes":261},"client/src/components/ui/slider.tsx":{"content":"import * as React from \"react\"\nimport * as SliderPrimitive from \"@radix-ui/react-slider\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Slider = React.forwardRef<\n React.ElementRef<typeof SliderPrimitive.Root>,\n React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>\n>(({ className, ...props }, ref) => (\n <SliderPrimitive.Root\n ref={ref}\n className={cn(\n \"relative flex w-full touch-none select-none items-center\",\n className\n )}\n {...props}\n >\n <SliderPrimitive.Track className=\"relative h-2 w-full grow overflow-hidden rounded-full bg-secondary\">\n <SliderPrimitive.Range className=\"absolute h-full bg-primary\" />\n </SliderPrimitive.Track>\n <SliderPrimitive.Thumb className=\"block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50\" />\n </SliderPrimitive.Root>\n))\nSlider.displayName = SliderPrimitive.Root.displayName\n\nexport { Slider }\n","size_bytes":1077},"client/src/components/ui/switch.tsx":{"content":"import * as React from \"react\"\nimport * as SwitchPrimitives from \"@radix-ui/react-switch\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Switch = React.forwardRef<\n React.ElementRef<typeof SwitchPrimitives.Root>,\n React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>\n>(({ className, ...props }, ref) => (\n <SwitchPrimitives.Root\n className={cn(\n \"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input\",\n className\n )}\n {...props}\n ref={ref}\n >\n <SwitchPrimitives.Thumb\n className={cn(\n \"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0\"\n )}\n />\n </SwitchPrimitives.Root>\n))\nSwitch.displayName = SwitchPrimitives.Root.displayName\n\nexport { Switch }\n","size_bytes":1139},"client/src/components/ui/table.tsx":{"content":"import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Table = React.forwardRef<\n HTMLTableElement,\n React.HTMLAttributes<HTMLTableElement>\n>(({ className, ...props }, ref) => (\n <div className=\"relative w-full overflow-auto\">\n <table\n ref={ref}\n className={cn(\"w-full caption-bottom text-sm\", className)}\n {...props}\n />\n </div>\n))\nTable.displayName = \"Table\"\n\nconst TableHeader = React.forwardRef<\n HTMLTableSectionElement,\n React.HTMLAttributes<HTMLTableSectionElement>\n>(({ className, ...props }, ref) => (\n <thead ref={ref} className={cn(\"[&_tr]:border-b\", className)} {...props} />\n))\nTableHeader.displayName = \"TableHeader\"\n\nconst TableBody = React.forwardRef<\n HTMLTableSectionElement,\n React.HTMLAttributes<HTMLTableSectionElement>\n>(({ className, ...props }, ref) => (\n <tbody\n ref={ref}\n className={cn(\"[&_tr:last-child]:border-0\", className)}\n {...props}\n />\n))\nTableBody.displayName = \"TableBody\"\n\nconst TableFooter = React.forwardRef<\n HTMLTableSectionElement,\n React.HTMLAttributes<HTMLTableSectionElement>\n>(({ className, ...props }, ref) => (\n <tfoot\n ref={ref}\n className={cn(\n \"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0\",\n className\n )}\n {...props}\n />\n))\nTableFooter.displayName = \"TableFooter\"\n\nconst TableRow = React.forwardRef<\n HTMLTableRowElement,\n React.HTMLAttributes<HTMLTableRowElement>\n>(({ className, ...props }, ref) => (\n <tr\n ref={ref}\n className={cn(\n \"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted\",\n className\n )}\n {...props}\n />\n))\nTableRow.displayName = \"TableRow\"\n\nconst TableHead = React.forwardRef<\n HTMLTableCellElement,\n React.ThHTMLAttributes<HTMLTableCellElement>\n>(({ className, ...props }, ref) => (\n <th\n ref={ref}\n className={cn(\n \"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0\",\n className\n )}\n {...props}\n />\n))\nTableHead.displayName = \"TableHead\"\n\nconst TableCell = React.forwardRef<\n HTMLTableCellElement,\n React.TdHTMLAttributes<HTMLTableCellElement>\n>(({ className, ...props }, ref) => (\n <td\n ref={ref}\n className={cn(\"p-4 align-middle [&:has([role=checkbox])]:pr-0\", className)}\n {...props}\n />\n))\nTableCell.displayName = \"TableCell\"\n\nconst TableCaption = React.forwardRef<\n HTMLTableCaptionElement,\n React.HTMLAttributes<HTMLTableCaptionElement>\n>(({ className, ...props }, ref) => (\n <caption\n ref={ref}\n className={cn(\"mt-4 text-sm text-muted-foreground\", className)}\n {...props}\n />\n))\nTableCaption.displayName = \"TableCaption\"\n\nexport {\n Table,\n TableHeader,\n TableBody,\n TableFooter,\n TableHead,\n TableRow,\n TableCell,\n TableCaption,\n}\n","size_bytes":2765},"client/src/components/ui/tabs.tsx":{"content":"import React from \"react\";\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\";\nimport { cn } from \"@/lib/utils\";\n\nconst Tabs = TabsPrimitive.Root;\n\nconst TabsList = React.forwardRef<\n React.ElementRef<typeof TabsPrimitive.List>,\n React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>\n>(({ className, ...props }, ref) => (\n <TabsPrimitive.List\n ref={ref}\n className={cn(\n \"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground\",\n className\n )}\n {...props}\n />\n));\nTabsList.displayName = TabsPrimitive.List.displayName;\n\nconst TabsTrigger = React.forwardRef<\n React.ElementRef<typeof TabsPrimitive.Trigger>,\n React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>\n>(({ className, ...props }, ref) => (\n <TabsPrimitive.Trigger\n ref={ref}\n className={cn(\n \"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 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-sm\",\n className\n )}\n {...props}\n />\n));\nTabsTrigger.displayName = TabsPrimitive.Trigger.displayName;\n\nconst TabsContent = React.forwardRef<\n React.ElementRef<typeof TabsPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>\n>(({ className, ...props }, ref) => (\n <TabsPrimitive.Content\n ref={ref}\n className={cn(\n \"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n className\n )}\n {...props}\n />\n));\nTabsContent.displayName = TabsPrimitive.Content.displayName;\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent };","size_bytes":1887},"client/src/components/ui/textarea.tsx":{"content":"import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Textarea = React.forwardRef<\n HTMLTextAreaElement,\n React.ComponentProps<\"textarea\">\n>(({ className, ...props }, ref) => {\n return (\n <textarea\n className={cn(\n \"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background 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 md:text-sm\",\n className\n )}\n ref={ref}\n {...props}\n />\n )\n})\nTextarea.displayName = \"Textarea\"\n\nexport { Textarea }\n","size_bytes":689},"client/src/components/ui/theme-toggle.tsx":{"content":"import { Button } from \"@/components/ui/button\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n DropdownMenuLabel,\n} from \"@/components/ui/dropdown-menu\";\nimport { useTheme } from \"@/contexts/theme-context\";\n\nexport function ThemeToggle() {\n const { theme, setTheme } = useTheme();\n\n const getThemeDisplay = () => {\n switch (theme) {\n case \"light\":\n return { icon: \"light_mode\", label: \"Light Theme\" };\n case \"dark\":\n return { icon: \"dark_mode\", label: \"Dark Theme\" };\n case \"amoled\":\n return { icon: \"phone_android\", label: \"AMOLED Theme\" };\n case \"system\":\n return { icon: \"computer\", label: \"System Theme\" };\n default:\n return { icon: \"palette\", label: \"Theme\" };\n }\n };\n\n const currentTheme = getThemeDisplay();\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button variant=\"ghost\" size=\"sm\" className=\"h-9 px-3\">\n <i className=\"material-icons text-base mr-2\">{currentTheme.icon}</i>\n <span className=\"hidden lg:inline text-sm\">{currentTheme.label}</span>\n <span className=\"lg:hidden text-sm\">Theme</span>\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" className=\"w-48\">\n <DropdownMenuLabel>Appearance</DropdownMenuLabel>\n <DropdownMenuSeparator />\n <DropdownMenuItem \n onClick={() => setTheme(\"light\")}\n className={theme === \"light\" ? \"bg-accent\" : \"\"}\n >\n <i className=\"material-icons mr-2 text-base\">light_mode</i>\n <div className=\"flex flex-col\">\n <span>Light</span>\n <span className=\"text-xs text-muted-foreground\">Bright and clean</span>\n </div>\n {theme === \"light\" && <i className=\"material-icons ml-auto text-sm\">check</i>}\n </DropdownMenuItem>\n <DropdownMenuItem \n onClick={() => setTheme(\"dark\")}\n className={theme === \"dark\" ? \"bg-accent\" : \"\"}\n >\n <i className=\"material-icons mr-2 text-base\">dark_mode</i>\n <div className=\"flex flex-col\">\n <span>Dark</span>\n <span className=\"text-xs text-muted-foreground\">Easy on the eyes</span>\n </div>\n {theme === \"dark\" && <i className=\"material-icons ml-auto text-sm\">check</i>}\n </DropdownMenuItem>\n <DropdownMenuItem \n onClick={() => setTheme(\"amoled\")}\n className={theme === \"amoled\" ? \"bg-accent\" : \"\"}\n >\n <i className=\"material-icons mr-2 text-base\">phone_android</i>\n <div className=\"flex flex-col\">\n <span>AMOLED</span>\n <span className=\"text-xs text-muted-foreground\">Pure black for OLED</span>\n </div>\n {theme === \"amoled\" && <i className=\"material-icons ml-auto text-sm\">check</i>}\n </DropdownMenuItem>\n <DropdownMenuSeparator />\n <DropdownMenuItem \n onClick={() => setTheme(\"system\")}\n className={theme === \"system\" ? \"bg-accent\" : \"\"}\n >\n <i className=\"material-icons mr-2 text-base\">computer</i>\n <div className=\"flex flex-col\">\n <span>System</span>\n <span className=\"text-xs text-muted-foreground\">Follows device setting</span>\n </div>\n {theme === \"system\" && <i className=\"material-icons ml-auto text-sm\">check</i>}\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n );\n}","size_bytes":3509},"client/src/components/ui/toast.tsx":{"content":"import * as React from \"react\"\nimport * as ToastPrimitives from \"@radix-ui/react-toast\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { X } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst ToastProvider = ToastPrimitives.Provider\n\nconst ToastViewport = React.forwardRef<\n React.ElementRef<typeof ToastPrimitives.Viewport>,\n React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>\n>(({ className, ...props }, ref) => (\n <ToastPrimitives.Viewport\n ref={ref}\n className={cn(\n \"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]\",\n className\n )}\n {...props}\n />\n))\nToastViewport.displayName = ToastPrimitives.Viewport.displayName\n\nconst toastVariants = cva(\n \"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 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\",\n {\n variants: {\n variant: {\n default: \"border bg-background text-foreground\",\n destructive:\n \"destructive group border-destructive bg-destructive text-destructive-foreground\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n }\n)\n\nconst Toast = React.forwardRef<\n React.ElementRef<typeof ToastPrimitives.Root>,\n React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &\n VariantProps<typeof toastVariants>\n>(({ className, variant, ...props }, ref) => {\n return (\n <ToastPrimitives.Root\n ref={ref}\n className={cn(toastVariants({ variant }), className)}\n {...props}\n />\n )\n})\nToast.displayName = ToastPrimitives.Root.displayName\n\nconst ToastAction = React.forwardRef<\n React.ElementRef<typeof ToastPrimitives.Action>,\n React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>\n>(({ className, ...props }, ref) => (\n <ToastPrimitives.Action\n ref={ref}\n className={cn(\n \"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 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\",\n className\n )}\n {...props}\n />\n))\nToastAction.displayName = ToastPrimitives.Action.displayName\n\nconst ToastClose = React.forwardRef<\n React.ElementRef<typeof ToastPrimitives.Close>,\n React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>\n>(({ className, ...props }, ref) => (\n <ToastPrimitives.Close\n ref={ref}\n className={cn(\n \"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 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\",\n className\n )}\n toast-close=\"\"\n {...props}\n >\n <X className=\"h-4 w-4\" />\n </ToastPrimitives.Close>\n))\nToastClose.displayName = ToastPrimitives.Close.displayName\n\nconst ToastTitle = React.forwardRef<\n React.ElementRef<typeof ToastPrimitives.Title>,\n React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>\n>(({ className, ...props }, ref) => (\n <ToastPrimitives.Title\n ref={ref}\n className={cn(\"text-sm font-semibold\", className)}\n {...props}\n />\n))\nToastTitle.displayName = ToastPrimitives.Title.displayName\n\nconst ToastDescription = React.forwardRef<\n React.ElementRef<typeof ToastPrimitives.Description>,\n React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>\n>(({ className, ...props }, ref) => (\n <ToastPrimitives.Description\n ref={ref}\n className={cn(\"text-sm opacity-90\", className)}\n {...props}\n />\n))\nToastDescription.displayName = ToastPrimitives.Description.displayName\n\ntype ToastProps = React.ComponentPropsWithoutRef<typeof Toast>\n\ntype ToastActionElement = React.ReactElement<typeof ToastAction>\n\nexport {\n type ToastProps,\n type ToastActionElement,\n ToastProvider,\n ToastViewport,\n Toast,\n ToastTitle,\n ToastDescription,\n ToastClose,\n ToastAction,\n}\n","size_bytes":4845},"client/src/components/ui/toaster.tsx":{"content":"import { useToast } from \"@/hooks/use-toast\"\nimport {\n Toast,\n ToastClose,\n ToastDescription,\n ToastProvider,\n ToastTitle,\n ToastViewport,\n} from \"@/components/ui/toast\"\n\nexport function Toaster() {\n const { toasts } = useToast()\n\n return (\n <ToastProvider>\n {toasts.map(function ({ id, title, description, action, ...props }) {\n return (\n <Toast key={id} {...props}>\n <div className=\"grid gap-1\">\n {title && <ToastTitle>{title}</ToastTitle>}\n {description && (\n <ToastDescription>{description}</ToastDescription>\n )}\n </div>\n {action}\n <ToastClose />\n </Toast>\n )\n })}\n <ToastViewport />\n </ToastProvider>\n )\n}\n","size_bytes":772},"client/src/components/ui/toggle-group.tsx":{"content":"\"use client\"\n\nimport * as React from \"react\"\nimport * as ToggleGroupPrimitive from \"@radix-ui/react-toggle-group\"\nimport { type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\nimport { toggleVariants } from \"@/components/ui/toggle\"\n\nconst ToggleGroupContext = React.createContext<\n VariantProps<typeof toggleVariants>\n>({\n size: \"default\",\n variant: \"default\",\n})\n\nconst ToggleGroup = React.forwardRef<\n React.ElementRef<typeof ToggleGroupPrimitive.Root>,\n React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> &\n VariantProps<typeof toggleVariants>\n>(({ className, variant, size, children, ...props }, ref) => (\n <ToggleGroupPrimitive.Root\n ref={ref}\n className={cn(\"flex items-center justify-center gap-1\", className)}\n {...props}\n >\n <ToggleGroupContext.Provider value={{ variant, size }}>\n {children}\n </ToggleGroupContext.Provider>\n </ToggleGroupPrimitive.Root>\n))\n\nToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName\n\nconst ToggleGroupItem = React.forwardRef<\n React.ElementRef<typeof ToggleGroupPrimitive.Item>,\n React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &\n VariantProps<typeof toggleVariants>\n>(({ className, children, variant, size, ...props }, ref) => {\n const context = React.useContext(ToggleGroupContext)\n\n return (\n <ToggleGroupPrimitive.Item\n ref={ref}\n className={cn(\n toggleVariants({\n variant: context.variant || variant,\n size: context.size || size,\n }),\n className\n )}\n {...props}\n >\n {children}\n </ToggleGroupPrimitive.Item>\n )\n})\n\nToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName\n\nexport { ToggleGroup, ToggleGroupItem }\n","size_bytes":1753},"client/src/components/ui/toggle.tsx":{"content":"import * as React from \"react\"\nimport * as TogglePrimitive from \"@radix-ui/react-toggle\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst toggleVariants = cva(\n \"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground 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=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 gap-2\",\n {\n variants: {\n variant: {\n default: \"bg-transparent\",\n outline:\n \"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground\",\n },\n size: {\n default: \"h-10 px-3 min-w-10\",\n sm: \"h-9 px-2.5 min-w-9\",\n lg: \"h-11 px-5 min-w-11\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n }\n)\n\nconst Toggle = React.forwardRef<\n React.ElementRef<typeof TogglePrimitive.Root>,\n React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &\n VariantProps<typeof toggleVariants>\n>(({ className, variant, size, ...props }, ref) => (\n <TogglePrimitive.Root\n ref={ref}\n className={cn(toggleVariants({ variant, size, className }))}\n {...props}\n />\n))\n\nToggle.displayName = TogglePrimitive.Root.displayName\n\nexport { Toggle, toggleVariants }\n","size_bytes":1527},"client/src/components/ui/tooltip.tsx":{"content":"\"use client\"\n\nimport * as React from \"react\"\nimport * as TooltipPrimitive from \"@radix-ui/react-tooltip\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst TooltipProvider = TooltipPrimitive.Provider\n\nconst Tooltip = TooltipPrimitive.Root\n\nconst TooltipTrigger = TooltipPrimitive.Trigger\n\nconst TooltipContent = React.forwardRef<\n React.ElementRef<typeof TooltipPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>\n>(({ className, sideOffset = 4, ...props }, ref) => (\n <TooltipPrimitive.Content\n ref={ref}\n sideOffset={sideOffset}\n className={cn(\n \"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-tooltip-content-transform-origin]\",\n className\n )}\n {...props}\n />\n))\nTooltipContent.displayName = TooltipPrimitive.Content.displayName\n\nexport { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }\n","size_bytes":1209},"client/src/components/ui/version-info.tsx":{"content":"import { useState, useEffect } from \"react\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Separator } from \"@/components/ui/separator\";\n\ninterface VersionInfo {\n version: string;\n name: string;\n build: string;\n features: string[];\n changelog: Record<string, {\n date: string;\n changes: string[];\n }>;\n}\n\nexport function VersionInfo() {\n const [versionData, setVersionData] = useState<VersionInfo | null>(null);\n\n useEffect(() => {\n fetch('/version.json')\n .then(res => res.json())\n .then(setVersionData)\n .catch(console.error);\n }, []);\n\n if (!versionData) return null;\n\n const latestVersion = Object.keys(versionData.changelog)[0];\n const latestChangelog = versionData.changelog[latestVersion];\n\n return (\n <Card className=\"w-full max-w-2xl mx-auto\">\n <CardHeader>\n <div className=\"flex items-center justify-between\">\n <CardTitle className=\"text-xl font-semibold\">\n {versionData.name}\n </CardTitle>\n <Badge variant=\"default\" className=\"text-sm\">\n v{versionData.version}\n </Badge>\n </div>\n <p className=\"text-sm text-gray-600 dark:text-gray-400\">\n Build: {versionData.build}\n </p>\n </CardHeader>\n \n <CardContent className=\"space-y-6\">\n {/* Key Features */}\n <div>\n <h3 className=\"font-medium mb-3\">Key Features</h3>\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-2\">\n {versionData.features.map((feature, index) => (\n <div key={index} className=\"flex items-center text-sm\">\n <i className=\"material-icons text-primary-600 text-sm mr-2\">check_circle</i>\n {feature}\n </div>\n ))}\n </div>\n </div>\n\n <Separator />\n\n {/* Latest Release Notes */}\n <div>\n <h3 className=\"font-medium mb-3\">\n Release Notes - v{latestVersion}\n </h3>\n <p className=\"text-sm text-gray-600 dark:text-gray-400 mb-3\">\n Released: {latestChangelog.date}\n </p>\n <ul className=\"space-y-2\">\n {latestChangelog.changes.map((change, index) => (\n <li key={index} className=\"flex items-start text-sm\">\n <i className=\"material-icons text-green-600 text-sm mr-2 mt-0.5\">\n new_releases\n </i>\n {change}\n </li>\n ))}\n </ul>\n </div>\n </CardContent>\n </Card>\n );\n}","size_bytes":2613}},"version":1}