From 94c7d5aec095d6562296c831f55245cd0286a525 Mon Sep 17 00:00:00 2001 From: Koncept Kit <63216427+konceptkit@users.noreply.github.com> Date: Fri, 5 Dec 2025 16:40:33 +0700 Subject: [PATCH] Initial Commit --- .env | 4 + .gitignore | 23 + components.json | 21 + craco.config.js | 112 + jsconfig.json | 9 + package.json | 94 + plugins/health-check/health-endpoints.js | 213 + plugins/health-check/webpack-health-plugin.js | 120 + postcss.config.js | 6 + public/index.html | 37 + src/App.css | 32 + src/App.js | 141 + src/components/AdminSidebar.js | 263 + src/components/AttendanceDialog.js | 114 + src/components/Navbar.js | 95 + src/components/PaymentActivationDialog.js | 330 + src/components/PlanDialog.js | 233 + src/components/ui/accordion.jsx | 41 + src/components/ui/alert-dialog.jsx | 97 + src/components/ui/alert.jsx | 47 + src/components/ui/aspect-ratio.jsx | 5 + src/components/ui/avatar.jsx | 33 + src/components/ui/badge.jsx | 34 + src/components/ui/breadcrumb.jsx | 92 + src/components/ui/button.jsx | 48 + src/components/ui/calendar.jsx | 71 + src/components/ui/card.jsx | 50 + src/components/ui/carousel.jsx | 193 + src/components/ui/checkbox.jsx | 22 + src/components/ui/collapsible.jsx | 9 + src/components/ui/command.jsx | 116 + src/components/ui/context-menu.jsx | 156 + src/components/ui/dialog.jsx | 94 + src/components/ui/drawer.jsx | 90 + src/components/ui/dropdown-menu.jsx | 156 + src/components/ui/form.jsx | 133 + src/components/ui/hover-card.jsx | 23 + src/components/ui/input-otp.jsx | 53 + src/components/ui/input.jsx | 19 + src/components/ui/label.jsx | 16 + src/components/ui/menubar.jsx | 198 + src/components/ui/navigation-menu.jsx | 104 + src/components/ui/pagination.jsx | 100 + src/components/ui/popover.jsx | 27 + src/components/ui/progress.jsx | 21 + src/components/ui/radio-group.jsx | 29 + src/components/ui/resizable.jsx | 40 + src/components/ui/scroll-area.jsx | 38 + src/components/ui/select.jsx | 119 + src/components/ui/separator.jsx | 23 + src/components/ui/sheet.jsx | 108 + src/components/ui/skeleton.jsx | 14 + src/components/ui/slider.jsx | 21 + src/components/ui/sonner.jsx | 28 + src/components/ui/switch.jsx | 22 + src/components/ui/table.jsx | 86 + src/components/ui/tabs.jsx | 41 + src/components/ui/textarea.jsx | 18 + src/components/ui/toast.jsx | 85 + src/components/ui/toaster.jsx | 33 + src/components/ui/toggle-group.jsx | 43 + src/components/ui/toggle.jsx | 40 + src/components/ui/tooltip.jsx | 26 + src/context/AuthContext.js | 81 + src/hooks/use-toast.js | 155 + src/index.css | 115 + src/index.js | 14 + src/layouts/AdminLayout.js | 72 + src/lib/utils.js | 6 + src/pages/Dashboard.js | 230 + src/pages/EventDetails.js | 201 + src/pages/Events.js | 132 + src/pages/Landing.js | 140 + src/pages/Login.js | 122 + src/pages/PaymentCancel.js | 116 + src/pages/PaymentSuccess.js | 135 + src/pages/Plans.js | 196 + src/pages/Profile.js | 230 + src/pages/Register.js | 388 + src/pages/VerifyEmail.js | 105 + src/pages/admin/AdminApprovals.js | 469 + src/pages/admin/AdminDashboard.js | 132 + src/pages/admin/AdminEvents.js | 430 + src/pages/admin/AdminMembers.js | 294 + src/pages/admin/AdminPlans.js | 365 + src/pages/admin/AdminStaff.js | 223 + src/pages/admin/AdminUserView.js | 151 + src/pages/admin/AdminUsers.js | 200 + src/utils/api.js | 17 + tailwind.config.js | 82 + yarn.lock | 10936 ++++++++++++++++ 91 files changed, 20446 insertions(+) create mode 100644 .env create mode 100644 .gitignore create mode 100644 components.json create mode 100644 craco.config.js create mode 100644 jsconfig.json create mode 100644 package.json create mode 100644 plugins/health-check/health-endpoints.js create mode 100644 plugins/health-check/webpack-health-plugin.js create mode 100644 postcss.config.js create mode 100644 public/index.html create mode 100644 src/App.css create mode 100644 src/App.js create mode 100644 src/components/AdminSidebar.js create mode 100644 src/components/AttendanceDialog.js create mode 100644 src/components/Navbar.js create mode 100644 src/components/PaymentActivationDialog.js create mode 100644 src/components/PlanDialog.js create mode 100644 src/components/ui/accordion.jsx create mode 100644 src/components/ui/alert-dialog.jsx create mode 100644 src/components/ui/alert.jsx create mode 100644 src/components/ui/aspect-ratio.jsx create mode 100644 src/components/ui/avatar.jsx create mode 100644 src/components/ui/badge.jsx create mode 100644 src/components/ui/breadcrumb.jsx create mode 100644 src/components/ui/button.jsx create mode 100644 src/components/ui/calendar.jsx create mode 100644 src/components/ui/card.jsx create mode 100644 src/components/ui/carousel.jsx create mode 100644 src/components/ui/checkbox.jsx create mode 100644 src/components/ui/collapsible.jsx create mode 100644 src/components/ui/command.jsx create mode 100644 src/components/ui/context-menu.jsx create mode 100644 src/components/ui/dialog.jsx create mode 100644 src/components/ui/drawer.jsx create mode 100644 src/components/ui/dropdown-menu.jsx create mode 100644 src/components/ui/form.jsx create mode 100644 src/components/ui/hover-card.jsx create mode 100644 src/components/ui/input-otp.jsx create mode 100644 src/components/ui/input.jsx create mode 100644 src/components/ui/label.jsx create mode 100644 src/components/ui/menubar.jsx create mode 100644 src/components/ui/navigation-menu.jsx create mode 100644 src/components/ui/pagination.jsx create mode 100644 src/components/ui/popover.jsx create mode 100644 src/components/ui/progress.jsx create mode 100644 src/components/ui/radio-group.jsx create mode 100644 src/components/ui/resizable.jsx create mode 100644 src/components/ui/scroll-area.jsx create mode 100644 src/components/ui/select.jsx create mode 100644 src/components/ui/separator.jsx create mode 100644 src/components/ui/sheet.jsx create mode 100644 src/components/ui/skeleton.jsx create mode 100644 src/components/ui/slider.jsx create mode 100644 src/components/ui/sonner.jsx create mode 100644 src/components/ui/switch.jsx create mode 100644 src/components/ui/table.jsx create mode 100644 src/components/ui/tabs.jsx create mode 100644 src/components/ui/textarea.jsx create mode 100644 src/components/ui/toast.jsx create mode 100644 src/components/ui/toaster.jsx create mode 100644 src/components/ui/toggle-group.jsx create mode 100644 src/components/ui/toggle.jsx create mode 100644 src/components/ui/tooltip.jsx create mode 100644 src/context/AuthContext.js create mode 100644 src/hooks/use-toast.js create mode 100644 src/index.css create mode 100644 src/index.js create mode 100644 src/layouts/AdminLayout.js create mode 100644 src/lib/utils.js create mode 100644 src/pages/Dashboard.js create mode 100644 src/pages/EventDetails.js create mode 100644 src/pages/Events.js create mode 100644 src/pages/Landing.js create mode 100644 src/pages/Login.js create mode 100644 src/pages/PaymentCancel.js create mode 100644 src/pages/PaymentSuccess.js create mode 100644 src/pages/Plans.js create mode 100644 src/pages/Profile.js create mode 100644 src/pages/Register.js create mode 100644 src/pages/VerifyEmail.js create mode 100644 src/pages/admin/AdminApprovals.js create mode 100644 src/pages/admin/AdminDashboard.js create mode 100644 src/pages/admin/AdminEvents.js create mode 100644 src/pages/admin/AdminMembers.js create mode 100644 src/pages/admin/AdminPlans.js create mode 100644 src/pages/admin/AdminStaff.js create mode 100644 src/pages/admin/AdminUserView.js create mode 100644 src/pages/admin/AdminUsers.js create mode 100644 src/utils/api.js create mode 100644 tailwind.config.js create mode 100644 yarn.lock diff --git a/.env b/.env new file mode 100644 index 0000000..43f07ff --- /dev/null +++ b/.env @@ -0,0 +1,4 @@ +REACT_APP_BACKEND_URL=http://localhost:8000 +WDS_SOCKET_PORT=443 +REACT_APP_ENABLE_VISUAL_EDITS=false +ENABLE_HEALTH_CHECK=false \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4d29575 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/components.json b/components.json new file mode 100644 index 0000000..ebf7e6e --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": false, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/craco.config.js b/craco.config.js new file mode 100644 index 0000000..d5a77ba --- /dev/null +++ b/craco.config.js @@ -0,0 +1,112 @@ +// craco.config.js +const path = require("path"); +require("dotenv").config(); + +// Environment variable overrides +const config = { + disableHotReload: process.env.DISABLE_HOT_RELOAD === "true", + enableVisualEdits: process.env.REACT_APP_ENABLE_VISUAL_EDITS === "true", + enableHealthCheck: process.env.ENABLE_HEALTH_CHECK === "true", +}; + +// Conditionally load visual editing modules only if enabled +let babelMetadataPlugin; +let setupDevServer; + +if (config.enableVisualEdits) { + babelMetadataPlugin = require("./plugins/visual-edits/babel-metadata-plugin"); + setupDevServer = require("./plugins/visual-edits/dev-server-setup"); +} + +// Conditionally load health check modules only if enabled +let WebpackHealthPlugin; +let setupHealthEndpoints; +let healthPluginInstance; + +if (config.enableHealthCheck) { + WebpackHealthPlugin = require("./plugins/health-check/webpack-health-plugin"); + setupHealthEndpoints = require("./plugins/health-check/health-endpoints"); + healthPluginInstance = new WebpackHealthPlugin(); +} + +const webpackConfig = { + webpack: { + alias: { + '@': path.resolve(__dirname, 'src'), + }, + configure: (webpackConfig) => { + + // Disable hot reload completely if environment variable is set + if (config.disableHotReload) { + // Remove hot reload related plugins + webpackConfig.plugins = webpackConfig.plugins.filter(plugin => { + return !(plugin.constructor.name === 'HotModuleReplacementPlugin'); + }); + + // Disable watch mode + webpackConfig.watch = false; + webpackConfig.watchOptions = { + ignored: /.*/, // Ignore all files + }; + } else { + // Add ignored patterns to reduce watched directories + webpackConfig.watchOptions = { + ...webpackConfig.watchOptions, + ignored: [ + '**/node_modules/**', + '**/.git/**', + '**/build/**', + '**/dist/**', + '**/coverage/**', + '**/public/**', + ], + }; + } + + // Add health check plugin to webpack if enabled + if (config.enableHealthCheck && healthPluginInstance) { + webpackConfig.plugins.push(healthPluginInstance); + } + + return webpackConfig; + }, + }, +}; + +// Only add babel plugin if visual editing is enabled +if (config.enableVisualEdits) { + webpackConfig.babel = { + plugins: [babelMetadataPlugin], + }; +} + +// Setup dev server with visual edits and/or health check +if (config.enableVisualEdits || config.enableHealthCheck) { + webpackConfig.devServer = (devServerConfig) => { + // Apply visual edits dev server setup if enabled + if (config.enableVisualEdits && setupDevServer) { + devServerConfig = setupDevServer(devServerConfig); + } + + // Add health check endpoints if enabled + if (config.enableHealthCheck && setupHealthEndpoints && healthPluginInstance) { + const originalSetupMiddlewares = devServerConfig.setupMiddlewares; + + devServerConfig.setupMiddlewares = (middlewares, devServer) => { + // Call original setup if exists + if (originalSetupMiddlewares) { + middlewares = originalSetupMiddlewares(middlewares, devServer); + } + + // Setup health endpoints + setupHealthEndpoints(devServer, healthPluginInstance); + + return middlewares; + }; + } + + return devServerConfig; + }; +} + +module.exports = webpackConfig; diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..822d8e4 --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src"] +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..75d497c --- /dev/null +++ b/package.json @@ -0,0 +1,94 @@ +{ + "name": "frontend", + "version": "0.1.0", + "private": true, + "dependencies": { + "@fontsource/dm-sans": "^5.2.8", + "@fontsource/fraunces": "^5.2.9", + "@hookform/resolvers": "^5.0.1", + "@radix-ui/react-accordion": "^1.2.8", + "@radix-ui/react-alert-dialog": "^1.1.11", + "@radix-ui/react-aspect-ratio": "^1.1.4", + "@radix-ui/react-avatar": "^1.1.7", + "@radix-ui/react-checkbox": "^1.2.3", + "@radix-ui/react-collapsible": "^1.1.8", + "@radix-ui/react-context-menu": "^2.2.12", + "@radix-ui/react-dialog": "^1.1.11", + "@radix-ui/react-dropdown-menu": "^2.1.12", + "@radix-ui/react-hover-card": "^1.1.11", + "@radix-ui/react-label": "^2.1.4", + "@radix-ui/react-menubar": "^1.1.12", + "@radix-ui/react-navigation-menu": "^1.2.10", + "@radix-ui/react-popover": "^1.1.11", + "@radix-ui/react-progress": "^1.1.4", + "@radix-ui/react-radio-group": "^1.3.4", + "@radix-ui/react-scroll-area": "^1.2.6", + "@radix-ui/react-select": "^2.2.2", + "@radix-ui/react-separator": "^1.1.4", + "@radix-ui/react-slider": "^1.3.2", + "@radix-ui/react-slot": "^1.2.0", + "@radix-ui/react-switch": "^1.2.2", + "@radix-ui/react-tabs": "^1.1.9", + "@radix-ui/react-toast": "^1.2.11", + "@radix-ui/react-toggle": "^1.1.6", + "@radix-ui/react-toggle-group": "^1.1.7", + "@radix-ui/react-tooltip": "^1.2.4", + "@stripe/react-stripe-js": "^2.0.0", + "@stripe/stripe-js": "^2.0.0", + "axios": "^1.8.4", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "cra-template": "1.2.0", + "date-fns": "^4.1.0", + "embla-carousel-react": "^8.6.0", + "input-otp": "^1.4.2", + "lucide-react": "^0.507.0", + "next-themes": "^0.4.6", + "react": "^19.0.0", + "react-day-picker": "8.10.1", + "react-dom": "^19.0.0", + "react-hook-form": "^7.56.2", + "react-icons": "^5.5.0", + "react-resizable-panels": "^3.0.1", + "react-router-dom": "^7.5.1", + "react-scripts": "5.0.1", + "sonner": "^2.0.3", + "tailwind-merge": "^3.2.0", + "tailwindcss-animate": "^1.0.7", + "vaul": "^1.1.2", + "zod": "^3.24.4" + }, + "scripts": { + "start": "craco start", + "build": "craco build", + "test": "craco test" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "@babel/plugin-proposal-private-property-in-object": "^7.21.11", + "@craco/craco": "^7.1.0", + "@eslint/js": "9.23.0", + "autoprefixer": "^10.4.20", + "eslint": "9.23.0", + "eslint-plugin-import": "2.31.0", + "eslint-plugin-jsx-a11y": "6.10.2", + "eslint-plugin-react": "7.37.4", + "eslint-plugin-react-hooks": "5.2.0", + "globals": "15.15.0", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.17" + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" +} diff --git a/plugins/health-check/health-endpoints.js b/plugins/health-check/health-endpoints.js new file mode 100644 index 0000000..5af66cf --- /dev/null +++ b/plugins/health-check/health-endpoints.js @@ -0,0 +1,213 @@ +// health-endpoints.js +// API endpoints for health checks and monitoring + +const os = require('os'); + +const SERVER_START_TIME = Date.now(); + +/** + * Setup health check endpoints on the dev server + * @param {Object} devServer - Webpack dev server instance + * @param {Object} healthPlugin - Instance of WebpackHealthPlugin + */ +function setupHealthEndpoints(devServer, healthPlugin) { + if (!devServer || !devServer.app) { + console.warn('[Health Check] Dev server not available, skipping health endpoints'); + return; + } + + if (!healthPlugin) { + console.warn('[Health Check] Health plugin not provided, skipping health endpoints'); + return; + } + + console.log('[Health Check] Setting up health endpoints...'); + + // ==================================================================== + // GET /health - Detailed health status (JSON) + // ==================================================================== + devServer.app.get("/health", (req, res) => { + const webpackStatus = healthPlugin.getStatus(); + const uptime = Date.now() - SERVER_START_TIME; + const memUsage = process.memoryUsage(); + + res.json({ + status: webpackStatus.isHealthy ? 'healthy' : 'unhealthy', + timestamp: new Date().toISOString(), + uptime: { + seconds: Math.floor(uptime / 1000), + formatted: formatDuration(uptime), + }, + webpack: { + state: webpackStatus.state, + isHealthy: webpackStatus.isHealthy, + hasCompiled: webpackStatus.hasCompiled, + errors: webpackStatus.errorCount, + warnings: webpackStatus.warningCount, + lastCompileTime: webpackStatus.lastCompileTime + ? new Date(webpackStatus.lastCompileTime).toISOString() + : null, + lastSuccessTime: webpackStatus.lastSuccessTime + ? new Date(webpackStatus.lastSuccessTime).toISOString() + : null, + compileDuration: webpackStatus.compileDuration + ? `${webpackStatus.compileDuration}ms` + : null, + totalCompiles: webpackStatus.totalCompiles, + firstCompileTime: webpackStatus.firstCompileTime + ? new Date(webpackStatus.firstCompileTime).toISOString() + : null, + }, + server: { + nodeVersion: process.version, + platform: os.platform(), + arch: os.arch(), + cpus: os.cpus().length, + memory: { + heapUsed: formatBytes(memUsage.heapUsed), + heapTotal: formatBytes(memUsage.heapTotal), + rss: formatBytes(memUsage.rss), + external: formatBytes(memUsage.external), + }, + systemMemory: { + total: formatBytes(os.totalmem()), + free: formatBytes(os.freemem()), + used: formatBytes(os.totalmem() - os.freemem()), + }, + }, + environment: process.env.NODE_ENV || 'development', + }); + }); + + // ==================================================================== + // GET /health/simple - Simple text response (OK/COMPILING/ERROR) + // ==================================================================== + devServer.app.get("/health/simple", (req, res) => { + const webpackStatus = healthPlugin.getSimpleStatus(); + + if (webpackStatus.state === 'success') { + res.status(200).send('OK'); + } else if (webpackStatus.state === 'compiling') { + res.status(200).send('COMPILING'); + } else if (webpackStatus.state === 'idle') { + res.status(200).send('IDLE'); + } else { + res.status(503).send('ERROR'); + } + }); + + // ==================================================================== + // GET /health/ready - Readiness check (Kubernetes/load balancer) + // ==================================================================== + devServer.app.get("/health/ready", (req, res) => { + const webpackStatus = healthPlugin.getSimpleStatus(); + + if (webpackStatus.state === 'success') { + res.status(200).json({ + ready: true, + state: webpackStatus.state, + }); + } else { + res.status(503).json({ + ready: false, + state: webpackStatus.state, + reason: webpackStatus.state === 'compiling' + ? 'Compilation in progress' + : 'Compilation failed', + }); + } + }); + + // ==================================================================== + // GET /health/live - Liveness check (Kubernetes) + // ==================================================================== + devServer.app.get("/health/live", (req, res) => { + res.status(200).json({ + alive: true, + timestamp: new Date().toISOString(), + }); + }); + + // ==================================================================== + // GET /health/errors - Get current errors and warnings + // ==================================================================== + devServer.app.get("/health/errors", (req, res) => { + const webpackStatus = healthPlugin.getStatus(); + + res.json({ + errorCount: webpackStatus.errorCount, + warningCount: webpackStatus.warningCount, + errors: webpackStatus.errors, + warnings: webpackStatus.warnings, + state: webpackStatus.state, + }); + }); + + // ==================================================================== + // GET /health/stats - Compilation statistics + // ==================================================================== + devServer.app.get("/health/stats", (req, res) => { + const webpackStatus = healthPlugin.getStatus(); + const uptime = Date.now() - SERVER_START_TIME; + + res.json({ + totalCompiles: webpackStatus.totalCompiles, + averageCompileTime: webpackStatus.totalCompiles > 0 + ? `${Math.round(uptime / webpackStatus.totalCompiles)}ms` + : null, + lastCompileDuration: webpackStatus.compileDuration + ? `${webpackStatus.compileDuration}ms` + : null, + firstCompileTime: webpackStatus.firstCompileTime + ? new Date(webpackStatus.firstCompileTime).toISOString() + : null, + serverUptime: formatDuration(uptime), + }); + }); + + console.log('[Health Check] ✓ Health endpoints ready:'); + console.log(' • GET /health - Detailed status'); + console.log(' • GET /health/simple - Simple OK/ERROR'); + console.log(' • GET /health/ready - Readiness check'); + console.log(' • GET /health/live - Liveness check'); + console.log(' • GET /health/errors - Error details'); + console.log(' • GET /health/stats - Statistics'); +} + +// ==================================================================== +// Helper Functions +// ==================================================================== + +/** + * Format bytes to human-readable string + * @param {number} bytes + * @returns {string} + */ +function formatBytes(bytes) { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; +} + +/** + * Format duration to human-readable string + * @param {number} ms - Duration in milliseconds + * @returns {string} + */ +function formatDuration(ms) { + const seconds = Math.floor(ms / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + + if (hours > 0) { + return `${hours}h ${minutes % 60}m ${seconds % 60}s`; + } else if (minutes > 0) { + return `${minutes}m ${seconds % 60}s`; + } else { + return `${seconds}s`; + } +} + +module.exports = setupHealthEndpoints; diff --git a/plugins/health-check/webpack-health-plugin.js b/plugins/health-check/webpack-health-plugin.js new file mode 100644 index 0000000..4efbce7 --- /dev/null +++ b/plugins/health-check/webpack-health-plugin.js @@ -0,0 +1,120 @@ +// webpack-health-plugin.js +// Webpack plugin that tracks compilation state and health metrics + +class WebpackHealthPlugin { + constructor() { + this.status = { + state: 'idle', // idle, compiling, success, failed + errors: [], + warnings: [], + lastCompileTime: null, + lastSuccessTime: null, + compileDuration: 0, + totalCompiles: 0, + firstCompileTime: null, + }; + } + + apply(compiler) { + const pluginName = 'WebpackHealthPlugin'; + + // Hook: Compilation started + compiler.hooks.compile.tap(pluginName, () => { + const now = Date.now(); + this.status.state = 'compiling'; + this.status.lastCompileTime = now; + + if (!this.status.firstCompileTime) { + this.status.firstCompileTime = now; + } + }); + + // Hook: Compilation completed + compiler.hooks.done.tap(pluginName, (stats) => { + const info = stats.toJson({ + all: false, + errors: true, + warnings: true, + }); + + this.status.totalCompiles++; + this.status.compileDuration = Date.now() - this.status.lastCompileTime; + + if (stats.hasErrors()) { + this.status.state = 'failed'; + this.status.errors = info.errors.map(err => ({ + message: err.message || String(err), + stack: err.stack, + moduleName: err.moduleName, + loc: err.loc, + })); + } else { + this.status.state = 'success'; + this.status.lastSuccessTime = Date.now(); + this.status.errors = []; + } + + if (stats.hasWarnings()) { + this.status.warnings = info.warnings.map(warn => ({ + message: warn.message || String(warn), + moduleName: warn.moduleName, + loc: warn.loc, + })); + } else { + this.status.warnings = []; + } + }); + + // Hook: Compilation failed + compiler.hooks.failed.tap(pluginName, (error) => { + this.status.state = 'failed'; + this.status.errors = [{ + message: error.message, + stack: error.stack, + }]; + this.status.compileDuration = Date.now() - this.status.lastCompileTime; + }); + + // Hook: Invalid (file changed, recompiling) + compiler.hooks.invalid.tap(pluginName, () => { + this.status.state = 'compiling'; + }); + } + + getStatus() { + return { + ...this.status, + // Add computed fields + isHealthy: this.status.state === 'success', + errorCount: this.status.errors.length, + warningCount: this.status.warnings.length, + hasCompiled: this.status.totalCompiles > 0, + }; + } + + // Get simplified status for quick checks + getSimpleStatus() { + return { + state: this.status.state, + isHealthy: this.status.state === 'success', + errorCount: this.status.errors.length, + warningCount: this.status.warnings.length, + }; + } + + // Reset statistics (useful for testing) + reset() { + this.status = { + state: 'idle', + errors: [], + warnings: [], + lastCompileTime: null, + lastSuccessTime: null, + compileDuration: 0, + totalCompiles: 0, + firstCompileTime: null, + }; + } +} + +module.exports = WebpackHealthPlugin; diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..33ad091 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..985a584 --- /dev/null +++ b/public/index.html @@ -0,0 +1,37 @@ + + + + + + + + + Membershi Website + + + + +
+ + + diff --git a/src/App.css b/src/App.css new file mode 100644 index 0000000..7f03c96 --- /dev/null +++ b/src/App.css @@ -0,0 +1,32 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', sans-serif; + background-color: #FDFCF8; + color: #3D405B; +} + +h1, h2, h3, h4, h5, h6 { + font-family: 'Fraunces', serif; + font-weight: 600; +} + +.fraunces { + font-family: 'Fraunces', serif; +} + +.dm-sans { + font-family: 'DM Sans', sans-serif; +} + +.bg-warm-gradient { + background: linear-gradient(135deg, rgba(242, 204, 143, 0.2) 0%, rgba(224, 122, 95, 0.2) 100%); +} + +.bg-soft-mesh { + background: radial-gradient(ellipse at top right, rgba(242, 204, 143, 0.4) 0%, #FDFCF8 50%, #FDFCF8 100%); +} diff --git a/src/App.js b/src/App.js new file mode 100644 index 0000000..1ed8773 --- /dev/null +++ b/src/App.js @@ -0,0 +1,141 @@ +import React from 'react'; +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; +import { Toaster } from './components/ui/sonner'; +import Landing from './pages/Landing'; +import Register from './pages/Register'; +import Login from './pages/Login'; +import VerifyEmail from './pages/VerifyEmail'; +import Dashboard from './pages/Dashboard'; +import Profile from './pages/Profile'; +import Events from './pages/Events'; +import EventDetails from './pages/EventDetails'; +import Plans from './pages/Plans'; +import PaymentSuccess from './pages/PaymentSuccess'; +import PaymentCancel from './pages/PaymentCancel'; +import AdminDashboard from './pages/admin/AdminDashboard'; +import AdminUsers from './pages/admin/AdminUsers'; +import AdminUserView from './pages/admin/AdminUserView'; +import AdminStaff from './pages/admin/AdminStaff'; +import AdminMembers from './pages/admin/AdminMembers'; +import AdminEvents from './pages/admin/AdminEvents'; +import AdminApprovals from './pages/admin/AdminApprovals'; +import AdminPlans from './pages/admin/AdminPlans'; +import AdminLayout from './layouts/AdminLayout'; +import { AuthProvider, useAuth } from './context/AuthContext'; + +const PrivateRoute = ({ children, adminOnly = false }) => { + const { user, loading } = useAuth(); + + if (loading) { + return
Loading...
; + } + + if (!user) { + return ; + } + + if (adminOnly && user.role !== 'admin') { + return ; + } + + return children; +}; + +function App() { + return ( + + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + + } /> + + + + } /> + + + + } /> + + + + } /> + + + + + + + } /> + + + + + + } /> + + + + + + } /> + + + + + + } /> + + + + + + } /> + + + + + + } /> + + + + + + } /> + + + + + + } /> + + + + + ); +} + +export default App; diff --git a/src/components/AdminSidebar.js b/src/components/AdminSidebar.js new file mode 100644 index 0000000..a697b07 --- /dev/null +++ b/src/components/AdminSidebar.js @@ -0,0 +1,263 @@ +import React, { useState, useEffect } from 'react'; +import { Link, useLocation, useNavigate } from 'react-router-dom'; +import { useAuth } from '../context/AuthContext'; +import api from '../utils/api'; +import { Badge } from './ui/badge'; +import { + LayoutDashboard, + UserCog, + Users, + CheckCircle, + CreditCard, + Calendar, + Shield, + Settings, + ChevronLeft, + ChevronRight, + LogOut, + Menu +} from 'lucide-react'; + +const AdminSidebar = ({ isOpen, onToggle, isMobile }) => { + const location = useLocation(); + const navigate = useNavigate(); + const { user, logout } = useAuth(); + const [pendingCount, setPendingCount] = useState(0); + + // Fetch pending approvals count + useEffect(() => { + const fetchPendingCount = async () => { + try { + const response = await api.get('/admin/users'); + const pending = response.data.filter(u => + ['pending_approval', 'pre_approved'].includes(u.status) + ); + setPendingCount(pending.length); + } catch (error) { + console.error('Failed to fetch pending count:', error); + } + }; + + fetchPendingCount(); + // Refresh count every 30 seconds + const interval = setInterval(fetchPendingCount, 30000); + return () => clearInterval(interval); + }, []); + + const handleLogout = () => { + logout(); + navigate('/login'); + }; + + const navItems = [ + { + name: 'Dashboard', + icon: LayoutDashboard, + path: '/admin', + disabled: false + }, + { + name: 'Staff', + icon: UserCog, + path: '/admin/staff', + disabled: false + }, + { + name: 'Members', + icon: Users, + path: '/admin/members', + disabled: false + }, + { + name: 'Approvals', + icon: CheckCircle, + path: '/admin/approvals', + disabled: false, + badge: pendingCount + }, + { + name: 'Plans', + icon: CreditCard, + path: '/admin/plans', + disabled: false + }, + { + name: 'Events', + icon: Calendar, + path: '/admin/events', + disabled: true + }, + { + name: 'Roles', + icon: Shield, + path: '/admin/roles', + disabled: false, + superadminOnly: true + } + ]; + + // Filter nav items based on user role + const filteredNavItems = navItems.filter(item => { + if (item.superadminOnly && user?.role !== 'superadmin') { + return false; + } + return true; + }); + + const isActive = (path) => { + if (path === '/admin') { + return location.pathname === '/admin'; + } + return location.pathname.startsWith(path); + }; + + return ( + <> + {/* Sidebar */} + + + ); +}; + +export default AdminSidebar; diff --git a/src/components/AttendanceDialog.js b/src/components/AttendanceDialog.js new file mode 100644 index 0000000..e1e116e --- /dev/null +++ b/src/components/AttendanceDialog.js @@ -0,0 +1,114 @@ +import React, { useState, useEffect } from 'react'; +import api from '../utils/api'; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from './ui/dialog'; +import { Button } from './ui/button'; +import { Checkbox } from './ui/checkbox'; +import { toast } from 'sonner'; + +export const AttendanceDialog = ({ event, open, onOpenChange, onSuccess }) => { + const [rsvps, setRsvps] = useState([]); + const [attendance, setAttendance] = useState({}); + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (open && event) { + fetchRSVPs(); + } + }, [open, event]); + + const fetchRSVPs = async () => { + try { + const response = await api.get(`/admin/events/${event.id}/rsvps`); + setRsvps(response.data); + + // Pre-populate with existing attendance + const attendanceMap = {}; + response.data.forEach(rsvp => { + attendanceMap[rsvp.user_id] = rsvp.attended || false; + }); + setAttendance(attendanceMap); + } catch (error) { + toast.error('Failed to fetch RSVPs'); + } + }; + + const handleSave = async () => { + setLoading(true); + try { + // Mark attendance for each user + for (const [userId, attended] of Object.entries(attendance)) { + await api.put(`/admin/events/${event.id}/attendance`, { + user_id: userId, + attended: attended + }); + } + + toast.success('Attendance updated successfully'); + onSuccess(); + onOpenChange(false); + } catch (error) { + toast.error('Failed to update attendance'); + } finally { + setLoading(false); + } + }; + + return ( + + + + + Mark Attendance: {event?.title} + + + +
+ {rsvps.length === 0 ? ( +

No RSVPs yet

+ ) : ( + rsvps.map((rsvp) => ( +
+ { + setAttendance({ ...attendance, [rsvp.user_id]: checked }); + }} + className="w-5 h-5" + /> +
+

{rsvp.user_name}

+

{rsvp.user_email}

+
+ {rsvp.attended && ( + + ✓ Attended + + )} +
+ )) + )} +
+ +
+ + +
+
+
+ ); +}; diff --git a/src/components/Navbar.js b/src/components/Navbar.js new file mode 100644 index 0000000..8a3a51c --- /dev/null +++ b/src/components/Navbar.js @@ -0,0 +1,95 @@ +import React from 'react'; +import { Link, useNavigate } from 'react-router-dom'; +import { useAuth } from '../context/AuthContext'; +import { Button } from './ui/button'; +import { Users, LogOut, LayoutDashboard } from 'lucide-react'; + +const Navbar = () => { + const { user, logout } = useAuth(); + const navigate = useNavigate(); + + const handleLogout = () => { + logout(); + navigate('/login'); + }; + + return ( + + ); +}; + +export default Navbar; diff --git a/src/components/PaymentActivationDialog.js b/src/components/PaymentActivationDialog.js new file mode 100644 index 0000000..9b048c2 --- /dev/null +++ b/src/components/PaymentActivationDialog.js @@ -0,0 +1,330 @@ +import React, { useState, useEffect } from 'react'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from './ui/dialog'; +import { Button } from './ui/button'; +import { Input } from './ui/input'; +import { Label } from './ui/label'; +import { Textarea } from './ui/textarea'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from './ui/select'; +import api from '../utils/api'; +import { toast } from 'sonner'; +import { Calendar } from 'lucide-react'; + +const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => { + const [loading, setLoading] = useState(false); + const [plans, setPlans] = useState([]); + const [selectedPlan, setSelectedPlan] = useState(null); + const [useCustomPeriod, setUseCustomPeriod] = useState(false); + const [formData, setFormData] = useState({ + plan_id: '', + amount: '', + payment_date: new Date().toISOString().split('T')[0], // Today's date + payment_method: 'cash', + custom_period_start: new Date().toISOString().split('T')[0], + custom_period_end: '', + notes: '' + }); + + // Fetch available plans on mount + useEffect(() => { + const fetchPlans = async () => { + try { + const response = await api.get('/admin/subscriptions/plans'); + setPlans(response.data.filter(p => p.active)); + } catch (error) { + toast.error('Failed to fetch subscription plans'); + } + }; + if (open) { + fetchPlans(); + } + }, [open]); + + // Update amount when plan changes (unless manually edited) + useEffect(() => { + if (selectedPlan && !formData.amount) { + setFormData(prev => ({ + ...prev, + amount: (selectedPlan.price_cents / 100).toFixed(2) + })); + } + }, [selectedPlan]); + + const handleSubmit = async (e) => { + e.preventDefault(); + + // Validation + if (!formData.plan_id) { + toast.error('Please select a subscription plan'); + return; + } + if (!formData.amount || parseFloat(formData.amount) <= 0) { + toast.error('Please enter a valid payment amount'); + return; + } + if (useCustomPeriod && (!formData.custom_period_start || !formData.custom_period_end)) { + toast.error('Please specify both start and end dates for custom period'); + return; + } + + setLoading(true); + try { + const payload = { + plan_id: formData.plan_id, + amount_cents: Math.round(parseFloat(formData.amount) * 100), + payment_date: new Date(formData.payment_date).toISOString(), + payment_method: formData.payment_method, + use_custom_period: useCustomPeriod, + notes: formData.notes || null + }; + + if (useCustomPeriod) { + payload.custom_period_start = new Date(formData.custom_period_start).toISOString(); + payload.custom_period_end = new Date(formData.custom_period_end).toISOString(); + } + + await api.post(`/admin/users/${user.id}/activate-payment`, payload); + + toast.success(`${user.first_name} ${user.last_name} activated successfully!`); + onOpenChange(false); + if (onSuccess) onSuccess(); + + // Reset form + setFormData({ + plan_id: '', + amount: '', + payment_date: new Date().toISOString().split('T')[0], + payment_method: 'cash', + custom_period_start: new Date().toISOString().split('T')[0], + custom_period_end: '', + notes: '' + }); + setUseCustomPeriod(false); + setSelectedPlan(null); + } catch (error) { + toast.error(error.response?.data?.detail || 'Failed to activate payment'); + } finally { + setLoading(false); + } + }; + + if (!user) return null; + + return ( + + + + + Activate Manual Payment + + + Record offline payment for {user.first_name} {user.last_name} ({user.email}) + + + +
+ {/* Subscription Plan Selection */} +
+ + + {selectedPlan && ( +

+ {selectedPlan.description || `${selectedPlan.billing_cycle} subscription`} +

+ )} +
+ + {/* Payment Amount */} +
+ + setFormData({...formData, amount: e.target.value})} + className="rounded-xl border-2 border-[#EAE0D5] focus:border-[#E07A5F]" + required + /> +

+ Amount can differ from plan price if offering a discount or partial payment +

+
+ + {/* Payment Date */} +
+ +
+ + setFormData({...formData, payment_date: e.target.value})} + className="pl-12 rounded-xl border-2 border-[#EAE0D5] focus:border-[#E07A5F]" + required + /> +
+
+ + {/* Payment Method */} +
+ + +
+ + {/* Subscription Period */} +
+ + +
+ setUseCustomPeriod(e.target.checked)} + className="rounded border-[#EAE0D5]" + /> + +
+ + {useCustomPeriod ? ( +
+
+ + setFormData({...formData, custom_period_start: e.target.value})} + className="rounded-xl border-2 border-[#EAE0D5] focus:border-[#E07A5F]" + required={useCustomPeriod} + /> +
+
+ + setFormData({...formData, custom_period_end: e.target.value})} + className="rounded-xl border-2 border-[#EAE0D5] focus:border-[#E07A5F]" + required={useCustomPeriod} + /> +
+
+ ) : ( + selectedPlan && ( +

+ Will use plan's billing cycle: {selectedPlan.billing_cycle} +
+ Starts today, ends {selectedPlan.billing_cycle === 'monthly' ? '30 days' : + selectedPlan.billing_cycle === 'quarterly' ? '90 days' : + selectedPlan.billing_cycle === 'yearly' ? '1 year' : + selectedPlan.billing_cycle === 'lifetime' ? 'lifetime' : '1 year'} from now +

+ ) + )} +
+ + {/* Notes */} +
+ +