@digilogiclabs/create-saas-app 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +975 -891
- package/dist/.tsbuildinfo +1 -1
- package/dist/generators/template-generator.d.ts +11 -0
- package/dist/generators/template-generator.d.ts.map +1 -1
- package/dist/generators/template-generator.js +389 -18
- package/dist/generators/template-generator.js.map +1 -1
- package/dist/index.js +1837 -0
- package/dist/index.js.map +1 -0
- package/dist/templates/infrastructure/kubernetes/base/template/README.md +253 -0
- package/dist/templates/infrastructure/kubernetes/base/template/configmap.yaml +12 -0
- package/dist/templates/infrastructure/kubernetes/base/template/deployment.yaml +123 -0
- package/dist/templates/infrastructure/kubernetes/base/template/hpa.yaml +45 -0
- package/dist/templates/infrastructure/kubernetes/base/template/ingress.yaml +31 -0
- package/dist/templates/infrastructure/kubernetes/base/template/kustomization.yaml +25 -0
- package/dist/templates/infrastructure/kubernetes/base/template/namespace.yaml +8 -0
- package/dist/templates/infrastructure/kubernetes/base/template/networkpolicy.yaml +48 -0
- package/dist/templates/infrastructure/kubernetes/base/template/pdb.yaml +14 -0
- package/dist/templates/infrastructure/kubernetes/base/template/secret.yaml +17 -0
- package/dist/templates/infrastructure/kubernetes/base/template/service.yaml +19 -0
- package/dist/templates/infrastructure/kubernetes/base/template/serviceaccount.yaml +9 -0
- package/dist/templates/infrastructure/kubernetes/production/template/kustomization.yaml +92 -0
- package/dist/templates/infrastructure/terraform/aws/template/README.md +156 -0
- package/dist/templates/infrastructure/terraform/aws/template/main.tf +343 -0
- package/dist/templates/infrastructure/terraform/aws/template/outputs.tf +66 -0
- package/dist/templates/infrastructure/terraform/aws/template/terraform.tfvars.example +28 -0
- package/dist/templates/infrastructure/terraform/aws/template/variables.tf +110 -0
- package/dist/templates/infrastructure/terraform/gcp/template/README.md +165 -0
- package/dist/templates/infrastructure/terraform/gcp/template/main.tf +397 -0
- package/dist/templates/infrastructure/terraform/gcp/template/outputs.tf +51 -0
- package/dist/templates/infrastructure/terraform/gcp/template/terraform.tfvars.example +29 -0
- package/dist/templates/infrastructure/terraform/gcp/template/variables.tf +115 -0
- package/dist/templates/shared/admin/web/app/admin/layout.tsx +34 -0
- package/dist/templates/shared/admin/web/components/admin-nav.tsx +48 -0
- package/dist/templates/shared/audit/web/lib/audit.ts +24 -0
- package/dist/templates/shared/auth/keycloak/web/app/api/auth/federated-logout/route.ts +173 -0
- package/dist/templates/shared/auth/keycloak/web/auth.config.ts +84 -0
- package/dist/templates/shared/auth/keycloak/web/auth.ts +26 -0
- package/dist/templates/shared/beta/web/app/api/beta-settings/route.ts +25 -0
- package/dist/templates/shared/beta/web/app/api/validate-beta-code/route.ts +67 -0
- package/dist/templates/shared/beta/web/lib/beta/settings.ts +31 -0
- package/dist/templates/shared/cache/web/lib/cache.ts +44 -0
- package/dist/templates/shared/config/web/lib/config.ts +112 -0
- package/dist/templates/shared/config/web/next.config.mjs +62 -0
- package/dist/templates/shared/contact/web/app/api/contact/route.ts +113 -0
- package/dist/templates/shared/contact/web/app/contact/page.tsx +195 -0
- package/dist/templates/shared/cookie-consent/web/components/cookie-consent.tsx +54 -0
- package/dist/templates/shared/database/postgresql/web/drizzle.config.ts +16 -0
- package/dist/templates/shared/database/postgresql/web/lib/db/drizzle.ts +39 -0
- package/dist/templates/shared/database/postgresql/web/lib/db/schema.ts +33 -0
- package/dist/templates/shared/database/supabase/web/lib/supabase/client.ts +12 -0
- package/dist/templates/shared/database/supabase/web/lib/supabase/server.ts +31 -0
- package/dist/templates/shared/database/supabase/web/lib/supabase/service.ts +15 -0
- package/dist/templates/shared/email/web/lib/email/branding.ts +18 -0
- package/dist/templates/shared/email/web/lib/email/client.ts +96 -0
- package/dist/templates/shared/error-pages/web/app/error.tsx +70 -0
- package/dist/templates/shared/error-pages/web/app/global-error.tsx +102 -0
- package/dist/templates/shared/error-pages/web/app/not-found.tsx +39 -0
- package/dist/templates/shared/health/web/app/api/health/route.ts +68 -0
- package/dist/templates/shared/legal/web/app/(legal)/privacy/page.tsx +205 -0
- package/dist/templates/shared/legal/web/app/(legal)/terms/page.tsx +154 -0
- package/dist/templates/shared/legal/web/lib/legal-config.ts +50 -0
- package/dist/templates/shared/loading/web/app/loading.tsx +5 -0
- package/dist/templates/shared/loading/web/components/skeleton.tsx +95 -0
- package/dist/templates/shared/middleware/web/middleware.ts +68 -0
- package/dist/templates/shared/observability/web/lib/observability.ts +135 -0
- package/dist/templates/shared/payments/web/app/api/webhooks/stripe/route.ts +109 -0
- package/dist/templates/shared/platform/web/lib/platform.ts +37 -0
- package/dist/templates/shared/redis/web/lib/rate-limit-store.ts +18 -0
- package/dist/templates/shared/redis/web/lib/redis.ts +48 -0
- package/dist/templates/shared/security/web/lib/api-security.ts +318 -0
- package/dist/templates/shared/seo/web/app/api/og/route.tsx +97 -0
- package/dist/templates/shared/seo/web/app/robots.ts +53 -0
- package/dist/templates/shared/seo/web/app/sitemap.ts +53 -0
- package/dist/templates/shared/utils/web/lib/api-response.ts +71 -0
- package/dist/templates/shared/utils/web/lib/utils.ts +85 -0
- package/dist/templates/web/ai-platform/template/.env.example +16 -0
- package/dist/templates/web/ai-platform/template/README.md +84 -0
- package/dist/templates/web/ai-platform/template/middleware.ts +55 -0
- package/dist/templates/web/ai-platform/template/next.config.js +14 -0
- package/dist/templates/web/ai-platform/template/package.json +55 -0
- package/dist/templates/web/ai-platform/template/src/app/api/chat/route.ts +54 -0
- package/dist/templates/web/ai-platform/template/src/app/chat/page.tsx +235 -0
- package/dist/templates/web/ai-platform/template/src/app/dashboard/page.tsx +142 -0
- package/dist/templates/web/ai-platform/template/src/app/globals.css +34 -0
- package/dist/templates/web/ai-platform/template/src/app/layout.tsx +27 -0
- package/dist/templates/web/ai-platform/template/src/app/page.tsx +203 -0
- package/dist/templates/web/ai-platform/template/src/components/providers/app-providers.tsx +27 -0
- package/dist/templates/web/ai-platform/template/src/lib/auth-server.ts +33 -0
- package/dist/templates/web/ai-platform/template/src/lib/supabase/client.ts +8 -0
- package/dist/templates/web/ai-platform/template/src/lib/supabase/server.ts +27 -0
- package/dist/templates/web/ai-platform/template/src/lib/utils.ts +6 -0
- package/dist/templates/web/ai-platform/template/tsconfig.json +27 -0
- package/dist/templates/web/iot-dashboard/template/.env.example +12 -0
- package/dist/templates/web/iot-dashboard/template/README.md +101 -0
- package/dist/templates/web/iot-dashboard/template/middleware.ts +56 -0
- package/dist/templates/web/iot-dashboard/template/next.config.js +14 -0
- package/dist/templates/web/iot-dashboard/template/package.json +49 -0
- package/dist/templates/web/iot-dashboard/template/src/app/dashboard/page.tsx +229 -0
- package/dist/templates/web/iot-dashboard/template/src/app/globals.css +20 -0
- package/dist/templates/web/iot-dashboard/template/src/app/layout.tsx +27 -0
- package/dist/templates/web/iot-dashboard/template/src/app/page.tsx +191 -0
- package/dist/templates/web/iot-dashboard/template/src/components/providers/app-providers.tsx +24 -0
- package/dist/templates/web/iot-dashboard/template/src/lib/auth-server.ts +33 -0
- package/dist/templates/web/iot-dashboard/template/src/lib/supabase/client.ts +8 -0
- package/dist/templates/web/iot-dashboard/template/src/lib/supabase/server.ts +27 -0
- package/dist/templates/web/iot-dashboard/template/src/lib/utils.ts +25 -0
- package/dist/templates/web/iot-dashboard/template/tsconfig.json +27 -0
- package/dist/templates/web/marketplace/template/.env.example +12 -0
- package/dist/templates/web/marketplace/template/README.md +66 -0
- package/dist/templates/web/marketplace/template/middleware.ts +56 -0
- package/dist/templates/web/marketplace/template/next.config.js +14 -0
- package/dist/templates/web/marketplace/template/package.json +51 -0
- package/dist/templates/web/marketplace/template/src/app/cart/page.tsx +147 -0
- package/dist/templates/web/marketplace/template/src/app/dashboard/page.tsx +149 -0
- package/dist/templates/web/marketplace/template/src/app/globals.css +20 -0
- package/dist/templates/web/marketplace/template/src/app/layout.tsx +27 -0
- package/dist/templates/web/marketplace/template/src/app/page.tsx +167 -0
- package/dist/templates/web/marketplace/template/src/app/products/page.tsx +129 -0
- package/dist/templates/web/marketplace/template/src/components/providers/app-providers.tsx +27 -0
- package/dist/templates/web/marketplace/template/src/lib/auth-server.ts +33 -0
- package/dist/templates/web/marketplace/template/src/lib/supabase/client.ts +8 -0
- package/dist/templates/web/marketplace/template/src/lib/supabase/server.ts +27 -0
- package/dist/templates/web/marketplace/template/src/lib/utils.ts +19 -0
- package/dist/templates/web/marketplace/template/tsconfig.json +27 -0
- package/dist/templates/web/micro-saas/template/.env.example +10 -0
- package/dist/templates/web/micro-saas/template/README.md +63 -0
- package/dist/templates/web/micro-saas/template/middleware.ts +53 -0
- package/dist/templates/web/micro-saas/template/next.config.js +14 -0
- package/dist/templates/web/micro-saas/template/package.json +41 -0
- package/dist/templates/web/micro-saas/template/src/app/dashboard/page.tsx +117 -0
- package/dist/templates/web/micro-saas/template/src/app/globals.css +20 -0
- package/dist/templates/web/micro-saas/template/src/app/layout.tsx +27 -0
- package/dist/templates/web/micro-saas/template/src/app/login/page.tsx +87 -0
- package/dist/templates/web/micro-saas/template/src/app/page.tsx +137 -0
- package/dist/templates/web/micro-saas/template/src/app/signup/page.tsx +108 -0
- package/dist/templates/web/micro-saas/template/src/components/providers/app-providers.tsx +24 -0
- package/dist/templates/web/micro-saas/template/src/lib/auth-server.ts +33 -0
- package/dist/templates/web/micro-saas/template/src/lib/supabase/client.ts +8 -0
- package/dist/templates/web/micro-saas/template/src/lib/supabase/server.ts +29 -0
- package/dist/templates/web/micro-saas/template/src/lib/utils.ts +6 -0
- package/dist/templates/web/micro-saas/template/tsconfig.json +27 -0
- package/package.json +5 -4
- package/src/templates/infrastructure/kubernetes/base/template/README.md +253 -0
- package/src/templates/infrastructure/kubernetes/base/template/configmap.yaml +12 -0
- package/src/templates/infrastructure/kubernetes/base/template/deployment.yaml +123 -0
- package/src/templates/infrastructure/kubernetes/base/template/hpa.yaml +45 -0
- package/src/templates/infrastructure/kubernetes/base/template/ingress.yaml +31 -0
- package/src/templates/infrastructure/kubernetes/base/template/kustomization.yaml +25 -0
- package/src/templates/infrastructure/kubernetes/base/template/namespace.yaml +8 -0
- package/src/templates/infrastructure/kubernetes/base/template/networkpolicy.yaml +48 -0
- package/src/templates/infrastructure/kubernetes/base/template/pdb.yaml +14 -0
- package/src/templates/infrastructure/kubernetes/base/template/secret.yaml +17 -0
- package/src/templates/infrastructure/kubernetes/base/template/service.yaml +19 -0
- package/src/templates/infrastructure/kubernetes/base/template/serviceaccount.yaml +9 -0
- package/src/templates/infrastructure/kubernetes/production/template/kustomization.yaml +92 -0
- package/src/templates/infrastructure/terraform/aws/template/README.md +156 -0
- package/src/templates/infrastructure/terraform/aws/template/main.tf +343 -0
- package/src/templates/infrastructure/terraform/aws/template/outputs.tf +66 -0
- package/src/templates/infrastructure/terraform/aws/template/terraform.tfvars.example +28 -0
- package/src/templates/infrastructure/terraform/aws/template/variables.tf +110 -0
- package/src/templates/infrastructure/terraform/gcp/template/README.md +165 -0
- package/src/templates/infrastructure/terraform/gcp/template/main.tf +397 -0
- package/src/templates/infrastructure/terraform/gcp/template/outputs.tf +51 -0
- package/src/templates/infrastructure/terraform/gcp/template/terraform.tfvars.example +29 -0
- package/src/templates/infrastructure/terraform/gcp/template/variables.tf +115 -0
- package/src/templates/shared/admin/web/app/admin/layout.tsx +34 -0
- package/src/templates/shared/admin/web/components/admin-nav.tsx +48 -0
- package/src/templates/shared/audit/web/lib/audit.ts +24 -0
- package/src/templates/shared/auth/keycloak/web/app/api/auth/federated-logout/route.ts +173 -0
- package/src/templates/shared/auth/keycloak/web/auth.config.ts +84 -0
- package/src/templates/shared/auth/keycloak/web/auth.ts +26 -0
- package/src/templates/shared/beta/web/app/api/beta-settings/route.ts +25 -0
- package/src/templates/shared/beta/web/app/api/validate-beta-code/route.ts +67 -0
- package/src/templates/shared/beta/web/lib/beta/settings.ts +31 -0
- package/src/templates/shared/cache/web/lib/cache.ts +44 -0
- package/src/templates/shared/config/web/lib/config.ts +112 -0
- package/src/templates/shared/config/web/next.config.mjs +62 -0
- package/src/templates/shared/contact/web/app/api/contact/route.ts +113 -0
- package/src/templates/shared/contact/web/app/contact/page.tsx +195 -0
- package/src/templates/shared/cookie-consent/web/components/cookie-consent.tsx +54 -0
- package/src/templates/shared/database/postgresql/web/drizzle.config.ts +16 -0
- package/src/templates/shared/database/postgresql/web/lib/db/drizzle.ts +39 -0
- package/src/templates/shared/database/postgresql/web/lib/db/schema.ts +33 -0
- package/src/templates/shared/database/supabase/web/lib/supabase/client.ts +12 -0
- package/src/templates/shared/database/supabase/web/lib/supabase/server.ts +31 -0
- package/src/templates/shared/database/supabase/web/lib/supabase/service.ts +15 -0
- package/src/templates/shared/email/web/lib/email/branding.ts +18 -0
- package/src/templates/shared/email/web/lib/email/client.ts +96 -0
- package/src/templates/shared/error-pages/web/app/error.tsx +70 -0
- package/src/templates/shared/error-pages/web/app/global-error.tsx +102 -0
- package/src/templates/shared/error-pages/web/app/not-found.tsx +39 -0
- package/src/templates/shared/health/web/app/api/health/route.ts +68 -0
- package/src/templates/shared/legal/web/app/(legal)/privacy/page.tsx +205 -0
- package/src/templates/shared/legal/web/app/(legal)/terms/page.tsx +154 -0
- package/src/templates/shared/legal/web/lib/legal-config.ts +50 -0
- package/src/templates/shared/loading/web/app/loading.tsx +5 -0
- package/src/templates/shared/loading/web/components/skeleton.tsx +95 -0
- package/src/templates/shared/middleware/web/middleware.ts +68 -0
- package/src/templates/shared/observability/web/lib/observability.ts +135 -0
- package/src/templates/shared/payments/web/app/api/webhooks/stripe/route.ts +109 -0
- package/src/templates/shared/platform/web/lib/platform.ts +37 -0
- package/src/templates/shared/redis/web/lib/rate-limit-store.ts +18 -0
- package/src/templates/shared/redis/web/lib/redis.ts +48 -0
- package/src/templates/shared/security/web/lib/api-security.ts +318 -0
- package/src/templates/shared/seo/web/app/api/og/route.tsx +97 -0
- package/src/templates/shared/seo/web/app/robots.ts +53 -0
- package/src/templates/shared/seo/web/app/sitemap.ts +53 -0
- package/src/templates/shared/utils/web/lib/api-response.ts +71 -0
- package/src/templates/shared/utils/web/lib/utils.ts +85 -0
- package/src/templates/web/ai-platform/template/.env.example +16 -0
- package/src/templates/web/ai-platform/template/README.md +84 -0
- package/src/templates/web/ai-platform/template/middleware.ts +55 -0
- package/src/templates/web/ai-platform/template/next.config.js +14 -0
- package/src/templates/web/ai-platform/template/package.json +55 -0
- package/src/templates/web/ai-platform/template/src/app/api/chat/route.ts +54 -0
- package/src/templates/web/ai-platform/template/src/app/chat/page.tsx +235 -0
- package/src/templates/web/ai-platform/template/src/app/dashboard/page.tsx +142 -0
- package/src/templates/web/ai-platform/template/src/app/globals.css +34 -0
- package/src/templates/web/ai-platform/template/src/app/layout.tsx +27 -0
- package/src/templates/web/ai-platform/template/src/app/page.tsx +203 -0
- package/src/templates/web/ai-platform/template/src/components/providers/app-providers.tsx +27 -0
- package/src/templates/web/ai-platform/template/src/lib/auth-server.ts +33 -0
- package/src/templates/web/ai-platform/template/src/lib/supabase/client.ts +8 -0
- package/src/templates/web/ai-platform/template/src/lib/supabase/server.ts +27 -0
- package/src/templates/web/ai-platform/template/src/lib/utils.ts +6 -0
- package/src/templates/web/ai-platform/template/tsconfig.json +27 -0
- package/src/templates/web/iot-dashboard/template/.env.example +12 -0
- package/src/templates/web/iot-dashboard/template/README.md +101 -0
- package/src/templates/web/iot-dashboard/template/middleware.ts +56 -0
- package/src/templates/web/iot-dashboard/template/next.config.js +14 -0
- package/src/templates/web/iot-dashboard/template/package.json +49 -0
- package/src/templates/web/iot-dashboard/template/src/app/dashboard/page.tsx +229 -0
- package/src/templates/web/iot-dashboard/template/src/app/globals.css +20 -0
- package/src/templates/web/iot-dashboard/template/src/app/layout.tsx +27 -0
- package/src/templates/web/iot-dashboard/template/src/app/page.tsx +191 -0
- package/src/templates/web/iot-dashboard/template/src/components/providers/app-providers.tsx +24 -0
- package/src/templates/web/iot-dashboard/template/src/lib/auth-server.ts +33 -0
- package/src/templates/web/iot-dashboard/template/src/lib/supabase/client.ts +8 -0
- package/src/templates/web/iot-dashboard/template/src/lib/supabase/server.ts +27 -0
- package/src/templates/web/iot-dashboard/template/src/lib/utils.ts +25 -0
- package/src/templates/web/iot-dashboard/template/tsconfig.json +27 -0
- package/src/templates/web/marketplace/template/.env.example +12 -0
- package/src/templates/web/marketplace/template/README.md +66 -0
- package/src/templates/web/marketplace/template/middleware.ts +56 -0
- package/src/templates/web/marketplace/template/next.config.js +14 -0
- package/src/templates/web/marketplace/template/package.json +51 -0
- package/src/templates/web/marketplace/template/src/app/cart/page.tsx +147 -0
- package/src/templates/web/marketplace/template/src/app/dashboard/page.tsx +149 -0
- package/src/templates/web/marketplace/template/src/app/globals.css +20 -0
- package/src/templates/web/marketplace/template/src/app/layout.tsx +27 -0
- package/src/templates/web/marketplace/template/src/app/page.tsx +167 -0
- package/src/templates/web/marketplace/template/src/app/products/page.tsx +129 -0
- package/src/templates/web/marketplace/template/src/components/providers/app-providers.tsx +27 -0
- package/src/templates/web/marketplace/template/src/lib/auth-server.ts +33 -0
- package/src/templates/web/marketplace/template/src/lib/supabase/client.ts +8 -0
- package/src/templates/web/marketplace/template/src/lib/supabase/server.ts +27 -0
- package/src/templates/web/marketplace/template/src/lib/utils.ts +19 -0
- package/src/templates/web/marketplace/template/tsconfig.json +27 -0
- package/src/templates/web/micro-saas/template/.env.example +10 -0
- package/src/templates/web/micro-saas/template/README.md +63 -0
- package/src/templates/web/micro-saas/template/middleware.ts +53 -0
- package/src/templates/web/micro-saas/template/next.config.js +14 -0
- package/src/templates/web/micro-saas/template/package.json +41 -0
- package/src/templates/web/micro-saas/template/src/app/dashboard/page.tsx +117 -0
- package/src/templates/web/micro-saas/template/src/app/globals.css +20 -0
- package/src/templates/web/micro-saas/template/src/app/layout.tsx +27 -0
- package/src/templates/web/micro-saas/template/src/app/login/page.tsx +87 -0
- package/src/templates/web/micro-saas/template/src/app/page.tsx +137 -0
- package/src/templates/web/micro-saas/template/src/app/signup/page.tsx +108 -0
- package/src/templates/web/micro-saas/template/src/components/providers/app-providers.tsx +24 -0
- package/src/templates/web/micro-saas/template/src/lib/auth-server.ts +33 -0
- package/src/templates/web/micro-saas/template/src/lib/supabase/client.ts +8 -0
- package/src/templates/web/micro-saas/template/src/lib/supabase/server.ts +29 -0
- package/src/templates/web/micro-saas/template/src/lib/utils.ts +6 -0
- package/src/templates/web/micro-saas/template/tsconfig.json +27 -0
- package/dist/cli/commands/add.d.ts +0 -6
- package/dist/cli/commands/add.d.ts.map +0 -1
- package/dist/cli/commands/add.js +0 -39
- package/dist/cli/commands/add.js.map +0 -1
- package/dist/cli/commands/create.d.ts +0 -45
- package/dist/cli/commands/create.d.ts.map +0 -1
- package/dist/cli/commands/create.js +0 -175
- package/dist/cli/commands/create.js.map +0 -1
- package/dist/cli/commands/index.d.ts +0 -4
- package/dist/cli/commands/index.d.ts.map +0 -1
- package/dist/cli/commands/index.js +0 -20
- package/dist/cli/commands/index.js.map +0 -1
- package/dist/cli/commands/update.d.ts +0 -6
- package/dist/cli/commands/update.d.ts.map +0 -1
- package/dist/cli/commands/update.js +0 -68
- package/dist/cli/commands/update.js.map +0 -1
- package/dist/cli/index.d.ts +0 -4
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/index.js +0 -61
- package/dist/cli/index.js.map +0 -1
- package/dist/cli/prompts/index.d.ts +0 -2
- package/dist/cli/prompts/index.d.ts.map +0 -1
- package/dist/cli/prompts/index.js +0 -18
- package/dist/cli/prompts/index.js.map +0 -1
- package/dist/cli/prompts/project-setup.d.ts +0 -5
- package/dist/cli/prompts/project-setup.d.ts.map +0 -1
- package/dist/cli/prompts/project-setup.js +0 -316
- package/dist/cli/prompts/project-setup.js.map +0 -1
- package/dist/cli/utils/git.d.ts +0 -9
- package/dist/cli/utils/git.d.ts.map +0 -1
- package/dist/cli/utils/git.js +0 -77
- package/dist/cli/utils/git.js.map +0 -1
- package/dist/cli/utils/index.d.ts +0 -5
- package/dist/cli/utils/index.d.ts.map +0 -1
- package/dist/cli/utils/index.js +0 -21
- package/dist/cli/utils/index.js.map +0 -1
- package/dist/cli/utils/logger.d.ts +0 -16
- package/dist/cli/utils/logger.d.ts.map +0 -1
- package/dist/cli/utils/logger.js +0 -55
- package/dist/cli/utils/logger.js.map +0 -1
- package/dist/cli/utils/package-manager.d.ts +0 -8
- package/dist/cli/utils/package-manager.d.ts.map +0 -1
- package/dist/cli/utils/package-manager.js +0 -92
- package/dist/cli/utils/package-manager.js.map +0 -1
- package/dist/cli/utils/spinner.d.ts +0 -7
- package/dist/cli/utils/spinner.d.ts.map +0 -1
- package/dist/cli/utils/spinner.js +0 -48
- package/dist/cli/utils/spinner.js.map +0 -1
- package/dist/cli/validators/dependencies.d.ts +0 -15
- package/dist/cli/validators/dependencies.d.ts.map +0 -1
- package/dist/cli/validators/dependencies.js +0 -108
- package/dist/cli/validators/dependencies.js.map +0 -1
- package/dist/cli/validators/index.d.ts +0 -3
- package/dist/cli/validators/index.d.ts.map +0 -1
- package/dist/cli/validators/index.js +0 -19
- package/dist/cli/validators/index.js.map +0 -1
- package/dist/cli/validators/project-name.d.ts +0 -5
- package/dist/cli/validators/project-name.d.ts.map +0 -1
- package/dist/cli/validators/project-name.js +0 -151
- package/dist/cli/validators/project-name.js.map +0 -1
- package/dist/generators/file-processor.d.ts +0 -28
- package/dist/generators/file-processor.d.ts.map +0 -1
- package/dist/generators/file-processor.js +0 -224
- package/dist/generators/file-processor.js.map +0 -1
- package/dist/generators/index.d.ts +0 -4
- package/dist/generators/index.d.ts.map +0 -1
- package/dist/generators/index.js +0 -20
- package/dist/generators/index.js.map +0 -1
- package/dist/generators/package-installer.d.ts +0 -29
- package/dist/generators/package-installer.d.ts.map +0 -1
- package/dist/generators/package-installer.js +0 -177
- package/dist/generators/package-installer.js.map +0 -1
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Beta Gate Client — thin wrapper over platform-core's createBetaClient.
|
|
3
|
+
*
|
|
4
|
+
* Pattern: each app provides its own config (endpoints, storage key,
|
|
5
|
+
* fail-safe defaults). The client handles all fetch/validate/store logic.
|
|
6
|
+
*
|
|
7
|
+
* Usage in components:
|
|
8
|
+
* import { fetchBetaSettings, validateInviteCode, storeBetaCode } from '@/lib/beta/settings'
|
|
9
|
+
*/
|
|
10
|
+
import { createBetaClient, type BetaClientConfig } from '@digilogiclabs/platform-core/auth';
|
|
11
|
+
|
|
12
|
+
export type { BetaSettings } from '@digilogiclabs/platform-core';
|
|
13
|
+
|
|
14
|
+
const betaConfig: BetaClientConfig = {
|
|
15
|
+
storageKey: 'app_beta_code',
|
|
16
|
+
settingsEndpoint: '/api/beta-settings',
|
|
17
|
+
validateEndpoint: '/api/validate-beta-code',
|
|
18
|
+
failSafeDefaults: {
|
|
19
|
+
betaMode: true,
|
|
20
|
+
requireInviteCode: true,
|
|
21
|
+
betaMessage: 'We are currently in private beta. Enter your invite code to continue.',
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const betaClient = createBetaClient(betaConfig);
|
|
26
|
+
|
|
27
|
+
export const fetchBetaSettings = betaClient.fetchSettings;
|
|
28
|
+
export const validateInviteCode = betaClient.validateCode;
|
|
29
|
+
export const storeBetaCode = betaClient.storeCode;
|
|
30
|
+
export const getStoredBetaCode = betaClient.getStoredCode;
|
|
31
|
+
export const clearStoredBetaCode = betaClient.clearStoredCode;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache utilities — TTL presets and HTTP cache header helpers.
|
|
3
|
+
*
|
|
4
|
+
* Provides standardized cache durations and response header generation
|
|
5
|
+
* for CDN and browser caching.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Standard cache TTL presets (in seconds) */
|
|
9
|
+
export const CacheTTL = {
|
|
10
|
+
/** 5 minutes — frequently changing data */
|
|
11
|
+
SHORT: 300,
|
|
12
|
+
/** 1 hour — moderately stable data */
|
|
13
|
+
MEDIUM: 3600,
|
|
14
|
+
/** 6 hours — mostly stable data */
|
|
15
|
+
LONG: 21600,
|
|
16
|
+
/** 24 hours — rarely changing data */
|
|
17
|
+
DAY: 86400,
|
|
18
|
+
/** 7 days — static content */
|
|
19
|
+
WEEK: 604800,
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Generate HTTP cache headers for API responses.
|
|
24
|
+
*
|
|
25
|
+
* Returns an object suitable for NextResponse headers or Response init.
|
|
26
|
+
* Supports CDN-specific headers for Cloudflare, Vercel, and standard proxies.
|
|
27
|
+
*
|
|
28
|
+
* @param ttl - Cache duration in seconds
|
|
29
|
+
* @param revalidate - Stale-while-revalidate window in seconds (default: ttl)
|
|
30
|
+
*/
|
|
31
|
+
export function getCacheHeaders(ttl: number, revalidate?: number): Record<string, string> {
|
|
32
|
+
const swr = revalidate ?? ttl;
|
|
33
|
+
return {
|
|
34
|
+
'Cache-Control': `public, s-maxage=${ttl}, stale-while-revalidate=${swr}`,
|
|
35
|
+
'CDN-Cache-Control': `public, s-maxage=${ttl}, stale-while-revalidate=${swr}`,
|
|
36
|
+
'Vercel-CDN-Cache-Control': `public, s-maxage=${ttl}, stale-while-revalidate=${swr}`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** No-cache headers — prevent all caching */
|
|
41
|
+
export const NO_CACHE_HEADERS: Record<string, string> = {
|
|
42
|
+
'Cache-Control': 'no-store, no-cache, must-revalidate',
|
|
43
|
+
Pragma: 'no-cache',
|
|
44
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized environment configuration — fail fast on missing secrets.
|
|
3
|
+
*
|
|
4
|
+
* Uses platform-core/auth helpers (getRequiredEnv, getOptionalEnv, checkEnvVars)
|
|
5
|
+
* so the app crashes immediately on deploy if critical env vars are missing,
|
|
6
|
+
* rather than failing silently at runtime when a request hits an unset secret.
|
|
7
|
+
*
|
|
8
|
+
* Skipped during build (NEXT_PHASE=phase-production-build) since env
|
|
9
|
+
* vars aren't available at build time in Docker/Nixpacks builds.
|
|
10
|
+
*
|
|
11
|
+
* Import this module from any server-side code to get validated, typed
|
|
12
|
+
* access to environment variables.
|
|
13
|
+
*/
|
|
14
|
+
import 'server-only';
|
|
15
|
+
import { getRequiredEnv, getOptionalEnv, checkEnvVars } from '@digilogiclabs/platform-core/auth';
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Startup validation — runs once on first import
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
const isBuildPhase = process.env.NEXT_PHASE === 'phase-production-build';
|
|
22
|
+
|
|
23
|
+
if (!isBuildPhase) {
|
|
24
|
+
const envResult = checkEnvVars({
|
|
25
|
+
required: ['AUTH_KEYCLOAK_ID', 'AUTH_KEYCLOAK_SECRET', 'AUTH_KEYCLOAK_ISSUER', 'AUTH_SECRET'],
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (!envResult.valid) {
|
|
29
|
+
console.error('[App] Missing required environment variables:', envResult.missing);
|
|
30
|
+
if (process.env.NODE_ENV === 'production') {
|
|
31
|
+
throw new Error(`Missing required environment variables: ${envResult.missing.join(', ')}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Centralized config — typed access to all env vars used across the app
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Lazy config that reads env vars on first property access.
|
|
42
|
+
* Required vars use getRequiredEnv (throws if missing).
|
|
43
|
+
* Optional vars use getOptionalEnv (returns default).
|
|
44
|
+
*
|
|
45
|
+
* NOTE: Auth vars (AUTH_KEYCLOAK_*) are intentionally omitted here because
|
|
46
|
+
* auth.config.ts reads them at module scope for NextAuth configuration, and
|
|
47
|
+
* that file cannot import server-only modules.
|
|
48
|
+
*/
|
|
49
|
+
export const config = {
|
|
50
|
+
/** Admin bearer token for protected API routes. */
|
|
51
|
+
get adminSecret(): string | undefined {
|
|
52
|
+
return process.env.ADMIN_SECRET;
|
|
53
|
+
},
|
|
54
|
+
/** Cron job bearer token. */
|
|
55
|
+
get cronSecret(): string | undefined {
|
|
56
|
+
return process.env.CRON_SECRET;
|
|
57
|
+
},
|
|
58
|
+
db: {
|
|
59
|
+
/** Database connection string. */
|
|
60
|
+
get url(): string {
|
|
61
|
+
return getRequiredEnv('DATABASE_URL');
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
redis: {
|
|
65
|
+
/** Redis connection URL. */
|
|
66
|
+
get url(): string | undefined {
|
|
67
|
+
return process.env.REDIS_URL;
|
|
68
|
+
},
|
|
69
|
+
/** Redis key prefix for namespace isolation. */
|
|
70
|
+
get keyPrefix(): string {
|
|
71
|
+
return getOptionalEnv('REDIS_KEY_PREFIX', 'app:');
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
email: {
|
|
75
|
+
/** Resend API key. */
|
|
76
|
+
get apiKey(): string | undefined {
|
|
77
|
+
return process.env.RESEND_API_KEY;
|
|
78
|
+
},
|
|
79
|
+
/** Sender address. */
|
|
80
|
+
get from(): string {
|
|
81
|
+
return getOptionalEnv('EMAIL_FROM', 'noreply@example.com');
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
stripe: {
|
|
85
|
+
/** Stripe secret key. */
|
|
86
|
+
get secretKey(): string {
|
|
87
|
+
return getRequiredEnv('STRIPE_SECRET_KEY');
|
|
88
|
+
},
|
|
89
|
+
/** Stripe webhook secret. */
|
|
90
|
+
get webhookSecret(): string {
|
|
91
|
+
return getRequiredEnv('STRIPE_WEBHOOK_SECRET');
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
/** Base URL for the app (used in email links, redirects). */
|
|
95
|
+
get baseUrl(): string {
|
|
96
|
+
return getOptionalEnv('NEXT_PUBLIC_APP_URL', 'http://localhost:3000');
|
|
97
|
+
},
|
|
98
|
+
/** App name. */
|
|
99
|
+
get appName(): string {
|
|
100
|
+
return getOptionalEnv('NEXT_PUBLIC_APP_NAME', 'My App');
|
|
101
|
+
},
|
|
102
|
+
beta: {
|
|
103
|
+
/** Master beta mode toggle. Default: true (closed beta). */
|
|
104
|
+
get mode(): boolean {
|
|
105
|
+
return process.env.BETA_MODE !== 'false';
|
|
106
|
+
},
|
|
107
|
+
/** Require invite code for signup. Default: true. */
|
|
108
|
+
get requireInviteCode(): boolean {
|
|
109
|
+
return process.env.REQUIRE_INVITE_CODE !== 'false';
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
} as const;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { generateSecurityHeaders } from '@digilogiclabs/platform-core/security-headers';
|
|
2
|
+
|
|
3
|
+
/** @type {import('next').NextConfig} */
|
|
4
|
+
const nextConfig = {
|
|
5
|
+
output: 'standalone',
|
|
6
|
+
|
|
7
|
+
// Security headers — CSP, HSTS, X-Frame-Options, etc.
|
|
8
|
+
async headers() {
|
|
9
|
+
const keycloakIssuer = process.env.AUTH_KEYCLOAK_ISSUER || '';
|
|
10
|
+
const keycloakOrigin = keycloakIssuer ? new URL(keycloakIssuer).origin : '';
|
|
11
|
+
|
|
12
|
+
return generateSecurityHeaders({
|
|
13
|
+
cspConnectSrc: [
|
|
14
|
+
keycloakOrigin,
|
|
15
|
+
'https://api.stripe.com',
|
|
16
|
+
].filter(Boolean),
|
|
17
|
+
cspFrameSrc: [
|
|
18
|
+
keycloakOrigin,
|
|
19
|
+
'https://js.stripe.com',
|
|
20
|
+
'https://hooks.stripe.com',
|
|
21
|
+
].filter(Boolean),
|
|
22
|
+
cspScriptSrc: ['https://js.stripe.com'],
|
|
23
|
+
cspImgSrc: ['https://lh3.googleusercontent.com'],
|
|
24
|
+
});
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
webpack: (config, { isServer }) => {
|
|
28
|
+
// Stub server-only packages for client bundles.
|
|
29
|
+
// Use $ anchor to match exact package name — preserves subpath imports
|
|
30
|
+
// like @digilogiclabs/platform-core/auth (Edge-safe).
|
|
31
|
+
if (!isServer) {
|
|
32
|
+
config.resolve.fallback = {
|
|
33
|
+
...config.resolve.fallback,
|
|
34
|
+
fs: false,
|
|
35
|
+
net: false,
|
|
36
|
+
tls: false,
|
|
37
|
+
dns: false,
|
|
38
|
+
child_process: false,
|
|
39
|
+
'node:stream': false,
|
|
40
|
+
'node:buffer': false,
|
|
41
|
+
'node:util': false,
|
|
42
|
+
'node:events': false,
|
|
43
|
+
'node:process': false,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
config.resolve.alias = {
|
|
47
|
+
...config.resolve.alias,
|
|
48
|
+
// Stub main barrel only — subpaths like /auth and /security-headers
|
|
49
|
+
// are Edge-safe and must NOT be stubbed.
|
|
50
|
+
'@digilogiclabs/platform-core$': false,
|
|
51
|
+
'ioredis$': false,
|
|
52
|
+
'pg$': false,
|
|
53
|
+
'nodemailer$': false,
|
|
54
|
+
'bullmq$': false,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return config;
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export default nextConfig;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contact Form API
|
|
3
|
+
*
|
|
4
|
+
* POST — Submit a contact form message.
|
|
5
|
+
* Public endpoint with rate limiting. Sends email via Resend.
|
|
6
|
+
*/
|
|
7
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { withPublicApi, escapeHtml } from '@/lib/api-security';
|
|
10
|
+
import { sendEmail } from '@/lib/email/client';
|
|
11
|
+
|
|
12
|
+
export const dynamic = 'force-dynamic';
|
|
13
|
+
|
|
14
|
+
const CATEGORY_LABELS: Record<string, string> = {
|
|
15
|
+
general: 'General Inquiry',
|
|
16
|
+
support: 'Technical Support',
|
|
17
|
+
business: 'Business Inquiry',
|
|
18
|
+
feedback: 'Feedback',
|
|
19
|
+
other: 'Other',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const ContactSchema = z.object({
|
|
23
|
+
name: z.string().min(2, 'Name must be at least 2 characters').max(100, 'Name is too long'),
|
|
24
|
+
email: z.string().email('Please enter a valid email address').max(255),
|
|
25
|
+
category: z.enum(['general', 'support', 'business', 'feedback', 'other'], {
|
|
26
|
+
errorMap: () => ({ message: 'Please select a category' }),
|
|
27
|
+
}),
|
|
28
|
+
message: z
|
|
29
|
+
.string()
|
|
30
|
+
.min(10, 'Message must be at least 10 characters')
|
|
31
|
+
.max(2000, 'Message is too long')
|
|
32
|
+
.refine((v) => !/<[^>]*>/.test(v), 'HTML tags are not allowed')
|
|
33
|
+
.refine(
|
|
34
|
+
(v) => !/(https?:\/\/|ftp:\/\/|www\.)\S+/i.test(v),
|
|
35
|
+
'Links are not allowed for security reasons'
|
|
36
|
+
),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export const POST = withPublicApi(
|
|
40
|
+
{
|
|
41
|
+
operation: 'contact/post',
|
|
42
|
+
rateLimit: { limit: 5, windowSeconds: 300 }, // 5 per 5 min — strict
|
|
43
|
+
},
|
|
44
|
+
async (request: NextRequest) => {
|
|
45
|
+
const body = await request.json();
|
|
46
|
+
const parsed = ContactSchema.safeParse(body);
|
|
47
|
+
|
|
48
|
+
if (!parsed.success) {
|
|
49
|
+
const errors: Record<string, string> = {};
|
|
50
|
+
for (const issue of parsed.error.issues) {
|
|
51
|
+
const field = String(issue.path[0] ?? 'input');
|
|
52
|
+
if (!errors[field]) errors[field] = issue.message;
|
|
53
|
+
}
|
|
54
|
+
return NextResponse.json({ error: 'Validation failed', errors }, { status: 400 });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const { name, email, category, message } = parsed.data;
|
|
58
|
+
const categoryLabel = CATEGORY_LABELS[category] ?? category;
|
|
59
|
+
|
|
60
|
+
// Build plain-text email
|
|
61
|
+
const text = [
|
|
62
|
+
`New Contact Form Submission`,
|
|
63
|
+
``,
|
|
64
|
+
`Name: ${name}`,
|
|
65
|
+
`Email: ${email}`,
|
|
66
|
+
`Category: ${categoryLabel}`,
|
|
67
|
+
``,
|
|
68
|
+
`Message:`,
|
|
69
|
+
message,
|
|
70
|
+
].join('\n');
|
|
71
|
+
|
|
72
|
+
// Build HTML email (escape all user input)
|
|
73
|
+
const html = `
|
|
74
|
+
<div style="font-family:sans-serif;max-width:600px;margin:0 auto">
|
|
75
|
+
<h2 style="border-bottom:2px solid #2563eb;padding-bottom:8px">New Contact Form Submission</h2>
|
|
76
|
+
<table style="width:100%;border-collapse:collapse">
|
|
77
|
+
<tr><td style="padding:8px 0;font-weight:bold;width:100px">Name:</td><td style="padding:8px 0">${escapeHtml(name)}</td></tr>
|
|
78
|
+
<tr><td style="padding:8px 0;font-weight:bold">Email:</td><td style="padding:8px 0">${escapeHtml(email)}</td></tr>
|
|
79
|
+
<tr><td style="padding:8px 0;font-weight:bold">Category:</td><td style="padding:8px 0">${escapeHtml(categoryLabel)}</td></tr>
|
|
80
|
+
</table>
|
|
81
|
+
<div style="background:#f3f4f6;padding:16px;border-radius:8px;margin-top:16px">
|
|
82
|
+
<strong>Message:</strong>
|
|
83
|
+
<p style="white-space:pre-wrap;margin:8px 0 0">${escapeHtml(message)}</p>
|
|
84
|
+
</div>
|
|
85
|
+
<p style="font-size:12px;color:#6b7280;margin-top:24px">
|
|
86
|
+
Sent from the contact form. Reply directly to respond to ${escapeHtml(name)}.
|
|
87
|
+
</p>
|
|
88
|
+
</div>`;
|
|
89
|
+
|
|
90
|
+
const adminEmail = process.env.ADMIN_EMAIL || process.env.EMAIL_FROM;
|
|
91
|
+
if (!adminEmail) {
|
|
92
|
+
console.error('[Contact] Neither ADMIN_EMAIL nor EMAIL_FROM is configured');
|
|
93
|
+
return NextResponse.json({ error: 'Server configuration error' }, { status: 500 });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const sent = await sendEmail({
|
|
97
|
+
to: adminEmail,
|
|
98
|
+
subject: `Contact: ${categoryLabel} — ${name}`,
|
|
99
|
+
html,
|
|
100
|
+
text,
|
|
101
|
+
replyTo: email,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (!sent) {
|
|
105
|
+
return NextResponse.json(
|
|
106
|
+
{ error: 'Failed to send message. Please try again later.' },
|
|
107
|
+
{ status: 502 }
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return NextResponse.json({ success: true });
|
|
112
|
+
}
|
|
113
|
+
);
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import type { FormEvent } from 'react';
|
|
5
|
+
|
|
6
|
+
const CATEGORIES = [
|
|
7
|
+
{ value: 'general', label: 'General Inquiry' },
|
|
8
|
+
{ value: 'support', label: 'Technical Support' },
|
|
9
|
+
{ value: 'business', label: 'Business Inquiry' },
|
|
10
|
+
{ value: 'feedback', label: 'Feedback' },
|
|
11
|
+
{ value: 'other', label: 'Other' },
|
|
12
|
+
] as const;
|
|
13
|
+
|
|
14
|
+
type Status = 'idle' | 'submitting' | 'success' | 'error';
|
|
15
|
+
|
|
16
|
+
export default function ContactPage() {
|
|
17
|
+
const [status, setStatus] = useState<Status>('idle');
|
|
18
|
+
const [errorMessage, setErrorMessage] = useState('');
|
|
19
|
+
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
|
|
20
|
+
|
|
21
|
+
async function handleSubmit(e: FormEvent<HTMLFormElement>) {
|
|
22
|
+
e.preventDefault();
|
|
23
|
+
setStatus('submitting');
|
|
24
|
+
setErrorMessage('');
|
|
25
|
+
setFieldErrors({});
|
|
26
|
+
|
|
27
|
+
const formData = new FormData(e.currentTarget);
|
|
28
|
+
const body = {
|
|
29
|
+
name: formData.get('name') as string,
|
|
30
|
+
email: formData.get('email') as string,
|
|
31
|
+
category: formData.get('category') as string,
|
|
32
|
+
message: formData.get('message') as string,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const res = await fetch('/api/contact', {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
headers: { 'Content-Type': 'application/json' },
|
|
39
|
+
body: JSON.stringify(body),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const data = await res.json();
|
|
43
|
+
|
|
44
|
+
if (!res.ok) {
|
|
45
|
+
if (data.errors) {
|
|
46
|
+
setFieldErrors(data.errors);
|
|
47
|
+
}
|
|
48
|
+
setErrorMessage(data.error || 'Something went wrong. Please try again.');
|
|
49
|
+
setStatus('error');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
setStatus('success');
|
|
54
|
+
(e.target as HTMLFormElement).reset();
|
|
55
|
+
} catch {
|
|
56
|
+
setErrorMessage('Network error. Please check your connection and try again.');
|
|
57
|
+
setStatus('error');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (status === 'success') {
|
|
62
|
+
return (
|
|
63
|
+
<div className="mx-auto max-w-2xl px-4 py-24 text-center">
|
|
64
|
+
<div className="rounded-2xl border border-green-200 bg-green-50 p-8 dark:border-green-800 dark:bg-green-950">
|
|
65
|
+
<h1 className="mb-4 text-2xl font-bold text-green-800 dark:text-green-200">
|
|
66
|
+
Message Sent
|
|
67
|
+
</h1>
|
|
68
|
+
<p className="text-green-700 dark:text-green-300">
|
|
69
|
+
Thank you for reaching out. We'll get back to you as soon as possible.
|
|
70
|
+
</p>
|
|
71
|
+
<button
|
|
72
|
+
onClick={() => setStatus('idle')}
|
|
73
|
+
className="mt-6 rounded-lg bg-green-600 px-6 py-2 text-sm font-medium text-white hover:bg-green-700"
|
|
74
|
+
>
|
|
75
|
+
Send Another Message
|
|
76
|
+
</button>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div className="mx-auto max-w-2xl px-4 py-16 sm:py-24">
|
|
84
|
+
<div className="mb-10 text-center">
|
|
85
|
+
<h1 className="text-3xl font-bold tracking-tight sm:text-4xl">Contact Us</h1>
|
|
86
|
+
<p className="mt-3 text-muted-foreground">
|
|
87
|
+
Have a question or need help? Fill out the form below and we'll get back to you.
|
|
88
|
+
</p>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<form onSubmit={handleSubmit} className="space-y-6">
|
|
92
|
+
{/* Name */}
|
|
93
|
+
<div>
|
|
94
|
+
<label htmlFor="name" className="mb-1.5 block text-sm font-medium">
|
|
95
|
+
Name
|
|
96
|
+
</label>
|
|
97
|
+
<input
|
|
98
|
+
id="name"
|
|
99
|
+
name="name"
|
|
100
|
+
type="text"
|
|
101
|
+
required
|
|
102
|
+
minLength={2}
|
|
103
|
+
maxLength={100}
|
|
104
|
+
className="w-full rounded-lg border border-input bg-background px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
|
105
|
+
placeholder="Your name"
|
|
106
|
+
/>
|
|
107
|
+
{fieldErrors.name && (
|
|
108
|
+
<p className="mt-1 text-sm text-red-500">{fieldErrors.name}</p>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
{/* Email */}
|
|
113
|
+
<div>
|
|
114
|
+
<label htmlFor="email" className="mb-1.5 block text-sm font-medium">
|
|
115
|
+
Email
|
|
116
|
+
</label>
|
|
117
|
+
<input
|
|
118
|
+
id="email"
|
|
119
|
+
name="email"
|
|
120
|
+
type="email"
|
|
121
|
+
required
|
|
122
|
+
maxLength={255}
|
|
123
|
+
className="w-full rounded-lg border border-input bg-background px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
|
124
|
+
placeholder="you@example.com"
|
|
125
|
+
/>
|
|
126
|
+
{fieldErrors.email && (
|
|
127
|
+
<p className="mt-1 text-sm text-red-500">{fieldErrors.email}</p>
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
{/* Category */}
|
|
132
|
+
<div>
|
|
133
|
+
<label htmlFor="category" className="mb-1.5 block text-sm font-medium">
|
|
134
|
+
Category
|
|
135
|
+
</label>
|
|
136
|
+
<select
|
|
137
|
+
id="category"
|
|
138
|
+
name="category"
|
|
139
|
+
required
|
|
140
|
+
defaultValue=""
|
|
141
|
+
className="w-full rounded-lg border border-input bg-background px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
|
142
|
+
>
|
|
143
|
+
<option value="" disabled>
|
|
144
|
+
Select a category
|
|
145
|
+
</option>
|
|
146
|
+
{CATEGORIES.map((cat) => (
|
|
147
|
+
<option key={cat.value} value={cat.value}>
|
|
148
|
+
{cat.label}
|
|
149
|
+
</option>
|
|
150
|
+
))}
|
|
151
|
+
</select>
|
|
152
|
+
{fieldErrors.category && (
|
|
153
|
+
<p className="mt-1 text-sm text-red-500">{fieldErrors.category}</p>
|
|
154
|
+
)}
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
{/* Message */}
|
|
158
|
+
<div>
|
|
159
|
+
<label htmlFor="message" className="mb-1.5 block text-sm font-medium">
|
|
160
|
+
Message
|
|
161
|
+
</label>
|
|
162
|
+
<textarea
|
|
163
|
+
id="message"
|
|
164
|
+
name="message"
|
|
165
|
+
required
|
|
166
|
+
minLength={10}
|
|
167
|
+
maxLength={2000}
|
|
168
|
+
rows={5}
|
|
169
|
+
className="w-full rounded-lg border border-input bg-background px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
|
170
|
+
placeholder="How can we help?"
|
|
171
|
+
/>
|
|
172
|
+
{fieldErrors.message && (
|
|
173
|
+
<p className="mt-1 text-sm text-red-500">{fieldErrors.message}</p>
|
|
174
|
+
)}
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
{/* Error banner */}
|
|
178
|
+
{status === 'error' && errorMessage && (
|
|
179
|
+
<div className="rounded-lg border border-red-200 bg-red-50 p-4 text-sm text-red-700 dark:border-red-800 dark:bg-red-950 dark:text-red-300">
|
|
180
|
+
{errorMessage}
|
|
181
|
+
</div>
|
|
182
|
+
)}
|
|
183
|
+
|
|
184
|
+
{/* Submit */}
|
|
185
|
+
<button
|
|
186
|
+
type="submit"
|
|
187
|
+
disabled={status === 'submitting'}
|
|
188
|
+
className="w-full rounded-lg bg-primary px-6 py-3 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
|
189
|
+
>
|
|
190
|
+
{status === 'submitting' ? 'Sending...' : 'Send Message'}
|
|
191
|
+
</button>
|
|
192
|
+
</form>
|
|
193
|
+
</div>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* GDPR cookie consent banner.
|
|
8
|
+
*
|
|
9
|
+
* Stores consent status in localStorage. Shows a fixed bottom banner
|
|
10
|
+
* until the user accepts or declines. Customize the text and styling
|
|
11
|
+
* to match your app's branding.
|
|
12
|
+
*/
|
|
13
|
+
export function CookieConsent() {
|
|
14
|
+
const [visible, setVisible] = useState(false);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const consent = localStorage.getItem('cookie-consent');
|
|
18
|
+
if (!consent) setVisible(true);
|
|
19
|
+
}, []);
|
|
20
|
+
|
|
21
|
+
if (!visible) return null;
|
|
22
|
+
|
|
23
|
+
const handleChoice = (choice: 'accepted' | 'declined') => {
|
|
24
|
+
localStorage.setItem('cookie-consent', choice);
|
|
25
|
+
setVisible(false);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className="fixed bottom-0 left-0 right-0 z-50 border-t border-gray-200 bg-white p-4 shadow-lg dark:border-gray-800 dark:bg-gray-950">
|
|
30
|
+
<div className="mx-auto flex max-w-5xl flex-col items-center gap-4 sm:flex-row sm:justify-between">
|
|
31
|
+
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
32
|
+
We use cookies to improve your experience.{' '}
|
|
33
|
+
<Link href="/privacy" className="underline hover:text-gray-900 dark:hover:text-gray-200">
|
|
34
|
+
Learn more
|
|
35
|
+
</Link>
|
|
36
|
+
</p>
|
|
37
|
+
<div className="flex gap-3">
|
|
38
|
+
<button
|
|
39
|
+
onClick={() => handleChoice('declined')}
|
|
40
|
+
className="rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800"
|
|
41
|
+
>
|
|
42
|
+
Decline
|
|
43
|
+
</button>
|
|
44
|
+
<button
|
|
45
|
+
onClick={() => handleChoice('accepted')}
|
|
46
|
+
className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700"
|
|
47
|
+
>
|
|
48
|
+
Accept
|
|
49
|
+
</button>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Config } from 'drizzle-kit';
|
|
2
|
+
import 'dotenv/config';
|
|
3
|
+
|
|
4
|
+
if (!process.env.DATABASE_URL) {
|
|
5
|
+
throw new Error('DATABASE_URL is not set in .env file');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
schema: './lib/db/schema.ts',
|
|
10
|
+
out: './lib/db/migrations',
|
|
11
|
+
dialect: 'postgresql',
|
|
12
|
+
dbCredentials: {
|
|
13
|
+
url: process.env.DATABASE_URL,
|
|
14
|
+
},
|
|
15
|
+
breakpoints: true,
|
|
16
|
+
} satisfies Config;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { drizzle, PostgresJsDatabase } from 'drizzle-orm/postgres-js';
|
|
2
|
+
import postgres, { Sql } from 'postgres';
|
|
3
|
+
import * as schema from './schema';
|
|
4
|
+
import { config } from '@/lib/config';
|
|
5
|
+
|
|
6
|
+
let _client: Sql | null = null;
|
|
7
|
+
let _db: PostgresJsDatabase<typeof schema> | null = null;
|
|
8
|
+
|
|
9
|
+
export function isDatabaseConfigured(): boolean {
|
|
10
|
+
return !!process.env.DATABASE_URL;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getClient(): Sql {
|
|
14
|
+
if (!_client) {
|
|
15
|
+
_client = postgres(config.db.url);
|
|
16
|
+
}
|
|
17
|
+
return _client;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getDatabase(): PostgresJsDatabase<typeof schema> {
|
|
21
|
+
if (!_db) {
|
|
22
|
+
_db = drizzle(getClient(), { schema });
|
|
23
|
+
}
|
|
24
|
+
return _db;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Lazy-initialized postgres-js client (defers connection until first use). */
|
|
28
|
+
export const client = new Proxy({} as Sql, {
|
|
29
|
+
get(_target, prop) {
|
|
30
|
+
return (getClient() as any)[prop];
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
/** Lazy-initialized Drizzle ORM instance with typed schema. */
|
|
35
|
+
export const db = new Proxy({} as PostgresJsDatabase<typeof schema>, {
|
|
36
|
+
get(_target, prop) {
|
|
37
|
+
return (getDatabase() as any)[prop];
|
|
38
|
+
},
|
|
39
|
+
});
|