DeeCeeXxx commited on
Commit
e9d5b7d
·
verified ·
1 Parent(s): af142c3

Upload 114 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +1 -0
  2. .gitignore +45 -0
  3. .idx/dev.nix +43 -0
  4. .modified +0 -0
  5. .vscode/settings.json +4 -0
  6. README.md +4 -10
  7. apphosting.yaml +7 -0
  8. components.json +21 -0
  9. docs/blueprint.md +21 -0
  10. next.config.ts +42 -0
  11. package-lock.json +0 -0
  12. package.json +67 -22
  13. postcss.config.mjs +8 -0
  14. src/.gitignore +45 -0
  15. src/.modified +0 -0
  16. src/README.md +5 -0
  17. src/ai/dev.ts +4 -0
  18. src/ai/flows/analyze-deployment-logs.ts +54 -0
  19. src/ai/genkit.ts +7 -0
  20. src/app/(auth)/login/page.tsx +31 -0
  21. src/app/(auth)/register/page.tsx +31 -0
  22. src/app/(dashboard)/deploy/page.tsx +9 -0
  23. src/app/(dashboard)/deployments/[id]/page.tsx +188 -0
  24. src/app/(dashboard)/layout.tsx +27 -0
  25. src/app/(dashboard)/page.tsx +88 -0
  26. src/app/admin/dashboard/page.tsx +270 -0
  27. src/app/admin/layout.tsx +54 -0
  28. src/app/admin/login/page.tsx +27 -0
  29. src/app/admin/stats/page.tsx +73 -0
  30. src/app/api/genkit/[...slug]/route.ts +4 -0
  31. src/app/dashboard/buy-coins/page.tsx +43 -0
  32. src/app/dashboard/deploy/page.tsx +11 -0
  33. src/app/dashboard/deployments/[id]/page.tsx +281 -0
  34. src/app/dashboard/layout.tsx +33 -0
  35. src/app/dashboard/page.tsx +244 -0
  36. src/app/dashboard/profile/page.tsx +185 -0
  37. src/app/favicon.ico +3 -0
  38. src/app/globals.css +118 -0
  39. src/app/layout.tsx +40 -0
  40. src/app/page.tsx +123 -0
  41. src/apphosting.yaml +7 -0
  42. src/components.json +21 -0
  43. src/components/admin/EditUserDialog.tsx +196 -0
  44. src/components/admin/StatCard.tsx +25 -0
  45. src/components/auth/AdminLoginForm.tsx +107 -0
  46. src/components/auth/LoginForm.tsx +106 -0
  47. src/components/auth/RegisterForm.tsx +144 -0
  48. src/components/billing/CoinPurchaseForm.tsx +412 -0
  49. src/components/deployment/AiLogAnalyzer.tsx +102 -0
  50. src/components/deployment/DeploymentCard.tsx +95 -0
.gitattributes CHANGED
@@ -37,3 +37,4 @@ davidcyrilapis-main/public/docs/background-music.mp3 filter=lfs diff=lfs merge=l
37
  public/docs/background-music.mp3 filter=lfs diff=lfs merge=lfs -text
38
  thumb.png filter=lfs diff=lfs merge=lfs -text
39
  Image/0.jpg filter=lfs diff=lfs merge=lfs -text
 
 
37
  public/docs/background-music.mp3 filter=lfs diff=lfs merge=lfs -text
38
  thumb.png filter=lfs diff=lfs merge=lfs -text
39
  Image/0.jpg filter=lfs diff=lfs merge=lfs -text
40
+ src/app/favicon.ico filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.*
7
+ .yarn/*
8
+ !.yarn/patches
9
+ !.yarn/plugins
10
+ !.yarn/releases
11
+ !.yarn/versions
12
+
13
+ # testing
14
+ /coverage
15
+
16
+ # next.js
17
+ /.next/
18
+ /out/
19
+
20
+ # production
21
+ /build
22
+
23
+ # misc
24
+ .DS_Store
25
+ *.pem
26
+
27
+ # debug
28
+ npm-debug.log*
29
+ yarn-debug.log*
30
+ yarn-error.log*
31
+ .pnpm-debug.log*
32
+
33
+ # vercel
34
+ .vercel
35
+
36
+ # typescript
37
+ *.tsbuildinfo
38
+ next-env.d.ts
39
+
40
+ .genkit/*
41
+ .env*
42
+
43
+ # firebase
44
+ firebase-debug.log
45
+ firestore-debug.log
.idx/dev.nix ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # To learn more about how to use Nix to configure your environment
2
+ # see: https://firebase.google.com/docs/studio/customize-workspace
3
+ {pkgs}: {
4
+ # Which nixpkgs channel to use.
5
+ channel = "stable-24.11"; # or "unstable"
6
+ # Use https://search.nixos.org/packages to find packages
7
+ packages = [
8
+ pkgs.nodejs_20
9
+ pkgs.zulu
10
+ ];
11
+ # Sets environment variables in the workspace
12
+ env = {};
13
+ # This adds a file watcher to startup the firebase emulators. The emulators will only start if
14
+ # a firebase.json file is written into the user's directory
15
+ services.firebase.emulators = {
16
+ detect = true;
17
+ projectId = "demo-app";
18
+ services = ["auth" "firestore"];
19
+ };
20
+ idx = {
21
+ # Search for the extensions you want on https://open-vsx.org/ and use "publisher.id"
22
+ extensions = [
23
+ # "vscodevim.vim"
24
+ ];
25
+ workspace = {
26
+ onCreate = {
27
+ default.openFiles = [
28
+ "src/app/page.tsx"
29
+ ];
30
+ };
31
+ };
32
+ # Enable previews and customize configuration
33
+ previews = {
34
+ enable = true;
35
+ previews = {
36
+ web = {
37
+ command = ["npm" "run" "dev" "--" "--port" "$PORT" "--hostname" "0.0.0.0"];
38
+ manager = "web";
39
+ };
40
+ };
41
+ };
42
+ };
43
+ }
.modified ADDED
File without changes
.vscode/settings.json ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ "IDX.aI.enableInlineCompletion": true,
3
+ "IDX.aI.enableCodebaseIndexing": true
4
+ }
README.md CHANGED
@@ -1,11 +1,5 @@
1
- ---
2
- title: Mywork
3
- emoji: 🌍
4
- colorFrom: green
5
- colorTo: purple
6
- sdk: docker
7
- pinned: false
8
- license: apache-2.0
9
- ---
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
1
+ # Firebase Studio
 
 
 
 
 
 
 
 
2
 
3
+ This is a NextJS starter in Firebase Studio.
4
+
5
+ To get started, take a look at src/app/page.tsx.
apphosting.yaml ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # Settings to manage and configure a Firebase App Hosting backend.
2
+ # https://firebase.google.com/docs/app-hosting/configure
3
+
4
+ runConfig:
5
+ # Increase this value if you'd like to automatically spin up
6
+ # more instances in response to increased traffic.
7
+ maxInstances: 1
components.json ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "default",
4
+ "rsc": true,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "tailwind.config.ts",
8
+ "css": "src/app/globals.css",
9
+ "baseColor": "neutral",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "aliases": {
14
+ "components": "@/components",
15
+ "utils": "@/lib/utils",
16
+ "ui": "@/components/ui",
17
+ "lib": "@/lib",
18
+ "hooks": "@/hooks"
19
+ },
20
+ "iconLibrary": "lucide"
21
+ }
docs/blueprint.md ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # **App Name**: Anita Deploy
2
+
3
+ ## Core Features:
4
+
5
+ - User Authentication: Login/Registration: Secure user authentication system to manage deployments.
6
+ - Repository Selection: GitHub Repo Input: Allow users to specify the Anita-V4 GitHub repository URL for deployment. Should include form validation for security.
7
+ - Environment Configuration: Environment Variable Input: Provide a form for users to input required environment variables (SESSION_ID, OWNER_NUMBER, BOT_NAME, etc.) with clear descriptions for each. Ensure that the form validates presence of mandatory variables.
8
+ - Deployment Controls: Deployment Control: Implement UI elements (buttons, toggles) for starting, stopping, and restarting the deployment.
9
+ - Automated Deployment: Heroku Integration: Use the provided Heroku API key to automate the deployment process to the user's Heroku account.
10
+ - Log Display: Real-time Logs: Display real-time deployment logs within the UI to provide feedback on the deployment progress and any errors.
11
+ - AI-Powered Debugging: Intelligent Issue Detection: Analyze deployment logs using an AI tool to identify common errors or warnings and suggest potential fixes or optimizations to the user.
12
+
13
+ ## Style Guidelines:
14
+
15
+ - Primary color: Vibrant blue (#29ABE2) to convey trust and stability, reflecting the app's dependable deployment capabilities.
16
+ - Background color: Light gray (#F0F0F0), offering a clean and modern backdrop that ensures readability and reduces visual fatigue.
17
+ - Accent color: Purple (#9C27B0), to highlight interactive elements such as buttons and links, complementing the primary blue and enhancing user engagement.
18
+ - Clean and modern sans-serif fonts to ensure readability and a professional look.
19
+ - Simple, outline-style icons to represent deployment status, settings, and other functionalities.
20
+ - Grid-based layout with clear sections for repository input, environment variables, and deployment controls.
21
+ - Subtle transition animations for a smooth and responsive user experience.
next.config.ts ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { NextConfig } from 'next';
2
+
3
+ const nextConfig: NextConfig = {
4
+ typescript: {
5
+ ignoreBuildErrors: true,
6
+ },
7
+ eslint: {
8
+ ignoreDuringBuilds: true,
9
+ },
10
+ images: {
11
+ remotePatterns: [
12
+ {
13
+ protocol: 'https',
14
+ hostname: 'placehold.co',
15
+ port: '',
16
+ pathname: '/**',
17
+ },
18
+ {
19
+ protocol: 'https',
20
+ hostname: 'firestuff.storage.googleapis.com',
21
+ port: '',
22
+ pathname: '/**',
23
+ },
24
+ {
25
+ protocol: 'https',
26
+ hostname: 'files.catbox.moe',
27
+ port: '',
28
+ pathname: '/**',
29
+ },
30
+ ],
31
+ },
32
+ webpack: (config) => {
33
+ config.resolve.fallback = {
34
+ ...config.resolve.fallback,
35
+ fs: false,
36
+ module: false,
37
+ };
38
+ return config;
39
+ },
40
+ };
41
+
42
+ export default nextConfig;
package-lock.json CHANGED
The diff for this file is too large to render. See raw diff
 
package.json CHANGED
@@ -1,27 +1,72 @@
1
  {
2
- "name": "Ebar-",
3
- "version": "1.0.0",
4
- "main": "index.js",
5
  "scripts": {
6
- "start": "node bot.js",
7
- "test": "echo \"Error: no test specified\" && exit 1"
 
 
 
 
 
8
  },
9
- "keywords": [],
10
- "author": "",
11
- "license": "ISC",
12
- "description": "",
13
  "dependencies": {
14
- "axios": "^1.7.7",
15
- "crypto": "^1.0.1",
16
- "express": "^4.21.1",
17
- "form-data": "^4.0.0",
18
- "mrnima-moviedl": "1.0.0",
19
- "node-telegram-bot-api": "^0.66.0",
20
- "webtorrent": "^2.5.10",
21
- "yt-search": "^2.12.1"
22
- },
23
- "keywords": ["Atomic", "bot", "Telegram", "multi-device"],
24
- "engines": {
25
- "node": "22.x"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  }
27
- }
 
1
  {
2
+ "name": "nextn",
3
+ "version": "0.1.0",
4
+ "private": true,
5
  "scripts": {
6
+ "dev": "next dev --turbopack -p 9002",
7
+ "genkit:dev": "genkit start -- tsx src/ai/dev.ts",
8
+ "genkit:watch": "genkit start -- tsx --watch src/ai/dev.ts",
9
+ "build": "next build",
10
+ "start": "next start",
11
+ "lint": "next lint",
12
+ "typecheck": "tsc --noEmit"
13
  },
 
 
 
 
14
  "dependencies": {
15
+ "@genkit-ai/googleai": "^1.8.0",
16
+ "@genkit-ai/next": "^1.8.0",
17
+ "@hookform/resolvers": "^4.1.3",
18
+ "@radix-ui/react-accordion": "^1.2.3",
19
+ "@radix-ui/react-alert-dialog": "^1.1.6",
20
+ "@radix-ui/react-avatar": "^1.1.3",
21
+ "@radix-ui/react-checkbox": "^1.1.4",
22
+ "@radix-ui/react-dialog": "^1.1.6",
23
+ "@radix-ui/react-dropdown-menu": "^2.1.6",
24
+ "@radix-ui/react-label": "^2.1.2",
25
+ "@radix-ui/react-menubar": "^1.1.6",
26
+ "@radix-ui/react-popover": "^1.1.6",
27
+ "@radix-ui/react-progress": "^1.1.2",
28
+ "@radix-ui/react-radio-group": "^1.2.3",
29
+ "@radix-ui/react-scroll-area": "^1.2.3",
30
+ "@radix-ui/react-select": "^2.1.6",
31
+ "@radix-ui/react-separator": "^1.1.2",
32
+ "@radix-ui/react-slider": "^1.2.3",
33
+ "@radix-ui/react-slot": "^1.1.2",
34
+ "@radix-ui/react-switch": "^1.1.3",
35
+ "@radix-ui/react-tabs": "^1.1.3",
36
+ "@radix-ui/react-toast": "^1.2.6",
37
+ "@radix-ui/react-tooltip": "^1.1.8",
38
+ "@opentelemetry/exporter-jaeger": "^2.0.1",
39
+ "@tanstack-query-firebase/react": "^1.0.5",
40
+ "@tanstack/react-query": "^5.66.0",
41
+ "bcryptjs": "^2.4.3",
42
+ "class-variance-authority": "^0.7.1",
43
+ "clsx": "^2.1.1",
44
+ "date-fns": "^3.6.0",
45
+ "dotenv": "^16.5.0",
46
+ "firebase": "^11.8.1",
47
+ "flutterwave-react-v3": "^1.3.2",
48
+ "genkit": "^1.8.0",
49
+ "lucide-react": "^0.475.0",
50
+ "mongodb": "^6.8.0",
51
+ "patch-package": "^8.0.0",
52
+ "react": "^18.3.1",
53
+ "react-day-picker": "^8.10.1",
54
+ "react-dom": "^18.3.1",
55
+ "react-hook-form": "^7.54.2",
56
+ "react-paystack": "^5.0.0",
57
+ "recharts": "^2.15.1",
58
+ "tailwind-merge": "^3.0.1",
59
+ "tailwindcss-animate": "^1.0.7",
60
+ "zod": "^3.24.2"
61
+ },
62
+ "devDependencies": {
63
+ "@types/bcryptjs": "^2.4.6",
64
+ "@types/node": "^20",
65
+ "@types/react": "^18",
66
+ "@types/react-dom": "^18",
67
+ "genkit-cli": "^1.8.0",
68
+ "postcss": "^8",
69
+ "tailwindcss": "^3.4.1",
70
+ "typescript": "^5"
71
  }
72
+ }
postcss.config.mjs ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('postcss-load-config').Config} */
2
+ const config = {
3
+ plugins: {
4
+ tailwindcss: {},
5
+ },
6
+ };
7
+
8
+ export default config;
src/.gitignore ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.*
7
+ .yarn/*
8
+ !.yarn/patches
9
+ !.yarn/plugins
10
+ !.yarn/releases
11
+ !.yarn/versions
12
+
13
+ # testing
14
+ /coverage
15
+
16
+ # next.js
17
+ /.next/
18
+ /out/
19
+
20
+ # production
21
+ /build
22
+
23
+ # misc
24
+ .DS_Store
25
+ *.pem
26
+
27
+ # debug
28
+ npm-debug.log*
29
+ yarn-debug.log*
30
+ yarn-error.log*
31
+ .pnpm-debug.log*
32
+
33
+ # vercel
34
+ .vercel
35
+
36
+ # typescript
37
+ *.tsbuildinfo
38
+ next-env.d.ts
39
+
40
+ .genkit/*
41
+ .env*
42
+
43
+ # firebase
44
+ firebase-debug.log
45
+ firestore-debug.log
src/.modified ADDED
File without changes
src/README.md ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # Firebase Studio
2
+
3
+ This is a NextJS starter in Firebase Studio.
4
+
5
+ To get started, take a look at src/app/page.tsx.
src/ai/dev.ts ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ import { config } from 'dotenv';
2
+ config();
3
+
4
+ import '@/ai/flows/analyze-deployment-logs.ts';
src/ai/flows/analyze-deployment-logs.ts ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use server';
2
+
3
+ /**
4
+ * @fileOverview Analyzes deployment logs to identify errors, warnings, and suggest fixes.
5
+ *
6
+ * - analyzeDeploymentLogs - A function that handles the analysis of deployment logs.
7
+ * - AnalyzeDeploymentLogsInput - The input type for the analyzeDeploymentLogs function.
8
+ * - AnalyzeDeploymentLogsOutput - The return type for the analyzeDeploymentLogs function.
9
+ */
10
+
11
+ import {ai} from '@/ai/genkit';
12
+ import {z} from 'genkit';
13
+
14
+ const AnalyzeDeploymentLogsInputSchema = z.object({
15
+ deploymentLogs: z
16
+ .string()
17
+ .describe('The deployment logs to analyze.'),
18
+ });
19
+ export type AnalyzeDeploymentLogsInput = z.infer<typeof AnalyzeDeploymentLogsInputSchema>;
20
+
21
+ const AnalyzeDeploymentLogsOutputSchema = z.object({
22
+ analysisResult: z.string().describe('The analysis result of the deployment logs, including identified errors, warnings, and suggested fixes.'),
23
+ });
24
+ export type AnalyzeDeploymentLogsOutput = z.infer<typeof AnalyzeDeploymentLogsOutputSchema>;
25
+
26
+ export async function analyzeDeploymentLogs(input: AnalyzeDeploymentLogsInput): Promise<AnalyzeDeploymentLogsOutput> {
27
+ return analyzeDeploymentLogsFlow(input);
28
+ }
29
+
30
+ const prompt = ai.definePrompt({
31
+ name: 'analyzeDeploymentLogsPrompt',
32
+ input: {schema: AnalyzeDeploymentLogsInputSchema},
33
+ output: {schema: AnalyzeDeploymentLogsOutputSchema},
34
+ prompt: `You are an AI expert in analyzing deployment logs for potential errors, warnings, and suggesting fixes.
35
+
36
+ Analyze the following deployment logs and provide a detailed analysis result including:
37
+ - Identified errors and warnings.
38
+ - Suggested fixes or optimizations to resolve deployment issues.
39
+
40
+ Deployment Logs:
41
+ {{{deploymentLogs}}}`,
42
+ });
43
+
44
+ const analyzeDeploymentLogsFlow = ai.defineFlow(
45
+ {
46
+ name: 'analyzeDeploymentLogsFlow',
47
+ inputSchema: AnalyzeDeploymentLogsInputSchema,
48
+ outputSchema: AnalyzeDeploymentLogsOutputSchema,
49
+ },
50
+ async input => {
51
+ const {output} = await prompt(input);
52
+ return output!;
53
+ }
54
+ );
src/ai/genkit.ts ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import {genkit} from 'genkit';
2
+ import {googleAI} from '@genkit-ai/googleai';
3
+
4
+ export const ai = genkit({
5
+ plugins: [googleAI()],
6
+ model: 'googleai/gemini-2.0-flash',
7
+ });
src/app/(auth)/login/page.tsx ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import { LoginForm } from "@/components/auth/LoginForm";
3
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
4
+ import Link from "next/link";
5
+ import { Zap } from "lucide-react";
6
+
7
+ export default function LoginPage() {
8
+ return (
9
+ <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-secondary via-background to-accent/20 p-4">
10
+ <Card className="w-full max-w-md shadow-xl hover:shadow-2xl">
11
+ <CardHeader className="text-center">
12
+ <Link href="/" className="inline-flex items-center justify-center mb-4">
13
+ <Zap className="h-8 w-8 text-primary" />
14
+ <span className="ml-2 text-2xl font-semibold text-foreground">Anita Deploy</span>
15
+ </Link>
16
+ <CardTitle className="text-2xl">Welcome Back!</CardTitle>
17
+ <CardDescription>Enter your credentials to access your dashboard.</CardDescription>
18
+ </CardHeader>
19
+ <CardContent>
20
+ <LoginForm />
21
+ <p className="mt-6 text-center text-sm text-muted-foreground">
22
+ Don&apos;t have an account?{" "}
23
+ <Link href="/register" className="font-medium text-primary hover:underline">
24
+ Sign up
25
+ </Link>
26
+ </p>
27
+ </CardContent>
28
+ </Card>
29
+ </div>
30
+ );
31
+ }
src/app/(auth)/register/page.tsx ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import { RegisterForm } from "@/components/auth/RegisterForm";
3
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
4
+ import Link from "next/link";
5
+ import { Zap } from "lucide-react";
6
+
7
+ export default function RegisterPage() {
8
+ return (
9
+ <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-secondary via-background to-accent/20 p-4">
10
+ <Card className="w-full max-w-md shadow-xl hover:shadow-2xl">
11
+ <CardHeader className="text-center">
12
+ <Link href="/" className="inline-flex items-center justify-center mb-4">
13
+ <Zap className="h-8 w-8 text-primary" />
14
+ <span className="ml-2 text-2xl font-semibold text-foreground">Anita Deploy</span>
15
+ </Link>
16
+ <CardTitle className="text-2xl">Create an Account</CardTitle>
17
+ <CardDescription>Join Anita Deploy to start deploying your bot.</CardDescription>
18
+ </CardHeader>
19
+ <CardContent>
20
+ <RegisterForm />
21
+ <p className="mt-6 text-center text-sm text-muted-foreground">
22
+ Already have an account?{" "}
23
+ <Link href="/login" className="font-medium text-primary hover:underline">
24
+ Log in
25
+ </Link>
26
+ </p>
27
+ </CardContent>
28
+ </Card>
29
+ </div>
30
+ );
31
+ }
src/app/(dashboard)/deploy/page.tsx ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import { DeploymentForm } from "@/components/deployment/DeploymentForm";
2
+
3
+ export default function NewDeploymentPage() {
4
+ return (
5
+ <div>
6
+ <DeploymentForm />
7
+ </div>
8
+ );
9
+ }
src/app/(dashboard)/deployments/[id]/page.tsx ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"; // This page uses client-side hooks for state and effects
2
+
3
+ import { useEffect, useState } from 'react';
4
+ import { useParams, useRouter } from 'next/navigation';
5
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
6
+ import { Badge } from '@/components/ui/badge';
7
+ import { Button } from '@/components/ui/button';
8
+ import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
9
+ import { LogDisplay } from '@/components/deployment/LogDisplay';
10
+ import { AiLogAnalyzer } from '@/components/deployment/AiLogAnalyzer';
11
+ import { DeploymentControls } from '@/components/deployment/DeploymentControls';
12
+ import type { Deployment, DeploymentStatus } from '@/lib/types';
13
+ import { ArrowLeft, CheckCircle2, ExternalLink, Hourglass, AlertTriangle, Zap, Info, PowerOff, Settings2, FileText, Brain } from 'lucide-react';
14
+ import { getDeploymentLogs } from '@/lib/actions/deployment'; // Mocked action
15
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
16
+ import { Skeleton } from '@/components/ui/skeleton';
17
+
18
+
19
+ // Mock function to fetch deployment details - replace with actual API call
20
+ async function fetchDeploymentDetails(id: string): Promise<Deployment | null> {
21
+ console.log("Fetching details for", id);
22
+ await new Promise(resolve => setTimeout(resolve, 500)); // Simulate API delay
23
+ const mockDeployments: Deployment[] = [
24
+ { id: 'anita-bot-alpha', appName: 'Anita Bot Alpha', status: 'succeeded', createdAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(), lastDeployedAt: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(), region: 'us-east', url: 'https://anita-bot-alpha.herokuapp.com' },
25
+ { id: 'anita-staging-v4', appName: 'Anita Staging V4', status: 'deploying', createdAt: new Date(Date.now() - 5 * 60 * 60 * 1000).toISOString(), region: 'eu-west' },
26
+ { id: 'legacy-anita-bot', appName: 'Legacy Anita Bot', status: 'failed', createdAt: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000).toISOString(), region: 'us-west' },
27
+ { id: 'anita-experimental', appName: 'Anita Experimental', status: 'stopped', createdAt: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString(), region: 'eu-central', url: 'https://anita-experimental.herokuapp.com' },
28
+ ];
29
+ return mockDeployments.find(d => d.id === id) || null;
30
+ }
31
+
32
+ function getStatusBadgeVariant(status: DeploymentStatus) {
33
+ switch (status) {
34
+ case 'succeeded': return 'default';
35
+ case 'deploying': return 'secondary';
36
+ case 'pending': return 'outline';
37
+ case 'failed': return 'destructive';
38
+ case 'stopped': return 'outline';
39
+ default: return 'outline';
40
+ }
41
+ }
42
+
43
+ function getStatusIcon(status: DeploymentStatus) {
44
+ switch (status) {
45
+ case 'succeeded': return <CheckCircle2 className="h-5 w-5 text-green-500" />;
46
+ case 'deploying': return <Hourglass className="h-5 w-5 text-blue-500 animate-spin" />;
47
+ case 'pending': return <Hourglass className="h-5 w-5 text-yellow-500" />;
48
+ case 'failed': return <AlertTriangle className="h-5 w-5 text-red-500" />;
49
+ case 'stopped': return <PowerOff className="h-5 w-5 text-gray-500" />;
50
+ default: return <Zap className="h-5 w-5 text-muted-foreground" />;
51
+ }
52
+ }
53
+
54
+
55
+ export default function DeploymentDetailPage() {
56
+ const params = useParams();
57
+ const router = useRouter();
58
+ const id = params.id as string;
59
+
60
+ const [deployment, setDeployment] = useState<Deployment | null>(null);
61
+ const [logs, setLogs] = useState<string[]>([]);
62
+ const [isLoading, setIsLoading] = useState(true);
63
+ const [logsLoading, setLogsLoading] = useState(true);
64
+
65
+ useEffect(() => {
66
+ if (id) {
67
+ setIsLoading(true);
68
+ fetchDeploymentDetails(id)
69
+ .then(data => {
70
+ setDeployment(data);
71
+ if (data) { // Fetch logs only if deployment data is found
72
+ setLogsLoading(true);
73
+ getDeploymentLogs(id).then(logData => {
74
+ setLogs(logData);
75
+ setLogsLoading(false);
76
+ });
77
+ }
78
+ })
79
+ .catch(err => console.error("Failed to fetch deployment details:", err))
80
+ .finally(() => setIsLoading(false));
81
+ }
82
+ }, [id]);
83
+
84
+ const handleStatusChange = (newStatus: DeploymentStatus) => {
85
+ if (deployment) {
86
+ setDeployment({ ...deployment, status: newStatus });
87
+ }
88
+ };
89
+
90
+ if (isLoading) {
91
+ return (
92
+ <div className="space-y-6">
93
+ <Skeleton className="h-8 w-1/4" />
94
+ <Skeleton className="h-24 w-full" />
95
+ <Skeleton className="h-64 w-full" />
96
+ <Skeleton className="h-64 w-full" />
97
+ </div>
98
+ );
99
+ }
100
+
101
+ if (!deployment) {
102
+ return (
103
+ <Alert variant="destructive">
104
+ <AlertTriangle className="h-4 w-4" />
105
+ <AlertTitle>Error</AlertTitle>
106
+ <AlertDescription>Deployment not found. It might have been deleted or the ID is incorrect.</AlertDescription>
107
+ <Button onClick={() => router.push('/dashboard')} variant="outline" className="mt-4">
108
+ <ArrowLeft className="mr-2 h-4 w-4" /> Back to Dashboard
109
+ </Button>
110
+ </Alert>
111
+ );
112
+ }
113
+
114
+ const combinedLogs = logs.join('\n');
115
+
116
+ return (
117
+ <div className="space-y-8">
118
+ <Button variant="outline" onClick={() => router.push('/dashboard')} className="mb-6">
119
+ <ArrowLeft className="mr-2 h-4 w-4" /> Back to Dashboard
120
+ </Button>
121
+
122
+ <Card className="shadow-lg">
123
+ <CardHeader>
124
+ <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-2">
125
+ <div>
126
+ <CardTitle className="text-3xl text-primary">{deployment.appName}</CardTitle>
127
+ <CardDescription>Manage your &quot;{deployment.appName}&quot; deployment.</CardDescription>
128
+ </div>
129
+ <Badge variant={getStatusBadgeVariant(deployment.status)} className="text-md capitalize px-3 py-1.5 flex items-center gap-2">
130
+ {getStatusIcon(deployment.status)}
131
+ {deployment.status}
132
+ </Badge>
133
+ </div>
134
+ </CardHeader>
135
+ <CardContent className="space-y-4">
136
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
137
+ <p><strong className="text-foreground/80">ID:</strong> {deployment.id}</p>
138
+ <p><strong className="text-foreground/80">Created:</strong> {new Date(deployment.createdAt).toLocaleString()}</p>
139
+ <p><strong className="text-foreground/80">Region:</strong> {deployment.region || 'N/A'}</p>
140
+ {deployment.lastDeployedAt && <p><strong className="text-foreground/80">Last Deployed:</strong> {new Date(deployment.lastDeployedAt).toLocaleString()}</p>}
141
+ </div>
142
+ {deployment.url && (
143
+ <a
144
+ href={deployment.url}
145
+ target="_blank"
146
+ rel="noopener noreferrer"
147
+ className="inline-flex items-center text-accent hover:underline"
148
+ >
149
+ <ExternalLink className="mr-2 h-4 w-4" /> Visit Deployed App
150
+ </a>
151
+ )}
152
+ <DeploymentControls deploymentId={deployment.id} currentStatus={deployment.status} onStatusChange={handleStatusChange} />
153
+ </CardContent>
154
+ </Card>
155
+
156
+ <Tabs defaultValue="logs" className="w-full">
157
+ <TabsList className="grid w-full grid-cols-2 md:grid-cols-3 gap-2 h-auto">
158
+ <TabsTrigger value="logs" className="py-2.5 text-sm"><FileText className="mr-2 h-4 w-4"/>Logs</TabsTrigger>
159
+ <TabsTrigger value="ai-analyzer" className="py-2.5 text-sm"><Brain className="mr-2 h-4 w-4"/>AI Analyzer</TabsTrigger>
160
+ <TabsTrigger value="settings" className="py-2.5 text-sm md:hidden lg:inline-block" disabled><Settings2 className="mr-2 h-4 w-4"/>Settings (Soon)</TabsTrigger>
161
+ </TabsList>
162
+ <TabsContent value="logs" className="mt-6">
163
+ <LogDisplay logs={logs} isLoading={logsLoading} />
164
+ </TabsContent>
165
+ <TabsContent value="ai-analyzer" className="mt-6">
166
+ <AiLogAnalyzer initialLogs={logsLoading ? "Loading logs for analysis..." : combinedLogs} />
167
+ </TabsContent>
168
+ <TabsContent value="settings" className="mt-6">
169
+ <Card>
170
+ <CardHeader>
171
+ <CardTitle className="flex items-center"><Settings2 className="mr-2 h-5 w-5 text-primary"/>Deployment Settings</CardTitle>
172
+ <CardDescription>Configuration options for this deployment (feature coming soon).</CardDescription>
173
+ </CardHeader>
174
+ <CardContent>
175
+ <Alert>
176
+ <Info className="h-4 w-4" />
177
+ <AlertTitle>Coming Soon!</AlertTitle>
178
+ <AlertDescription>
179
+ Advanced settings and environment variable management for this deployment will be available here in a future update.
180
+ </AlertDescription>
181
+ </Alert>
182
+ </CardContent>
183
+ </Card>
184
+ </TabsContent>
185
+ </Tabs>
186
+ </div>
187
+ );
188
+ }
src/app/(dashboard)/layout.tsx ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Header } from "@/components/layout/Header";
2
+ import { ReactNode } from "react";
3
+
4
+ interface DashboardLayoutProps {
5
+ children: ReactNode;
6
+ }
7
+
8
+ export default function DashboardLayout({ children }: DashboardLayoutProps) {
9
+ // Here you would typically add authentication checks.
10
+ // For this example, we assume the user is authenticated.
11
+ // e.g., by checking a session or token.
12
+ // If not authenticated, redirect to /login.
13
+
14
+ return (
15
+ <div className="flex min-h-screen flex-col">
16
+ <Header />
17
+ <main className="flex-1 container py-8">
18
+ {children}
19
+ </main>
20
+ <footer className="py-6 border-t">
21
+ <div className="container text-center text-sm text-muted-foreground">
22
+ Anita Deploy &copy; {new Date().getFullYear()}
23
+ </div>
24
+ </footer>
25
+ </div>
26
+ );
27
+ }
src/app/(dashboard)/page.tsx ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Link from 'next/link';
2
+ import { Button } from '@/components/ui/button';
3
+ import { PlusCircle, LayoutGrid } from 'lucide-react';
4
+ import { DeploymentCard } from '@/components/deployment/DeploymentCard';
5
+ import type { Deployment } from '@/lib/types';
6
+
7
+
8
+ // Mock data for deployments - in a real app, this would come from an API/database
9
+ const mockDeployments: Deployment[] = [
10
+ {
11
+ id: 'anita-bot-alpha',
12
+ appName: 'Anita Bot Alpha',
13
+ status: 'succeeded',
14
+ createdAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(), // 2 days ago
15
+ lastDeployedAt: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(), // 1 day ago
16
+ region: 'us-east',
17
+ url: 'https://anita-bot-alpha.herokuapp.com',
18
+ userId: ''
19
+ },
20
+ {
21
+ id: 'anita-staging-v4',
22
+ appName: 'Anita Staging V4',
23
+ status: 'deploying',
24
+ createdAt: new Date(Date.now() - 5 * 60 * 60 * 1000).toISOString(), // 5 hours ago
25
+ region: 'eu-west',
26
+ userId: ''
27
+ },
28
+ {
29
+ id: 'legacy-anita-bot',
30
+ appName: 'Legacy Anita Bot',
31
+ status: 'failed',
32
+ createdAt: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000).toISOString(), // 10 days ago
33
+ region: 'us-west',
34
+ userId: ''
35
+ },
36
+ {
37
+ id: 'anita-experimental',
38
+ appName: 'Anita Experimental',
39
+ status: 'stopped',
40
+ createdAt: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString(),
41
+ region: 'eu-central',
42
+ url: 'https://anita-experimental.herokuapp.com',
43
+ userId: ''
44
+ },
45
+ ];
46
+
47
+ export const dynamic = 'force-dynamic'
48
+
49
+ export default function DashboardPage() {
50
+ const deployments = mockDeployments; // In real app: fetchDeployments();
51
+
52
+ return (
53
+ <div className="space-y-8">
54
+ <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
55
+ <div>
56
+ <h1 className="text-3xl font-bold tracking-tight text-foreground">Your Deployments</h1>
57
+ <p className="text-muted-foreground">Manage your Anita-V4 bot deployments.</p>
58
+ </div>
59
+ <Button asChild size="lg">
60
+ <Link href="/dashboard/deploy">
61
+ <PlusCircle className="mr-2 h-5 w-5" /> New Deployment
62
+ </Link>
63
+ </Button>
64
+ </div>
65
+
66
+ {deployments.length > 0 ? (
67
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
68
+ {deployments.map((deployment) => (
69
+ <DeploymentCard key={deployment.id} deployment={deployment} />
70
+ ))}
71
+ </div>
72
+ ) : (
73
+ <div className="text-center py-12 border-2 border-dashed rounded-lg">
74
+ <LayoutGrid className="mx-auto h-12 w-12 text-muted-foreground mb-4" />
75
+ <h3 className="text-xl font-semibold text-foreground">No Deployments Yet</h3>
76
+ <p className="text-muted-foreground mt-1">
77
+ Get started by creating your first Anita-V4 bot deployment.
78
+ </p>
79
+ <Button asChild className="mt-6">
80
+ <Link href="/dashboard/deploy">
81
+ <PlusCircle className="mr-2 h-4 w-4" /> Create Deployment
82
+ </Link>
83
+ </Button>
84
+ </div>
85
+ )}
86
+ </div>
87
+ );
88
+ }
src/app/admin/dashboard/page.tsx ADDED
@@ -0,0 +1,270 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ "use client";
3
+
4
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
5
+ import { Button } from "@/components/ui/button";
6
+ import { Input } from "@/components/ui/input";
7
+ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, FormDescription } from "@/components/ui/form";
8
+ import { useForm } from "react-hook-form";
9
+ import { zodResolver } from "@hookform/resolvers/zod";
10
+ import { PlatformApiKeySchema, type PlatformApiKeyInput } from "@/lib/schemas";
11
+ import { getPlatformApiKey, updatePlatformApiKey, getAllUsersForAdmin } from "@/lib/actions/admin";
12
+ import { useToast } from "@/hooks/use-toast";
13
+ import { useEffect, useState, useCallback, useMemo } from "react";
14
+ import { Shield, KeyRound, Users, FileText, Loader2, Eye, EyeOff, RefreshCcw, Edit, Search } from "lucide-react";
15
+ import type { User } from "@/lib/types";
16
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
17
+ import { Badge } from "@/components/ui/badge";
18
+ import { ScrollArea } from "@/components/ui/scroll-area";
19
+ import { EditUserDialog } from "@/components/admin/EditUserDialog";
20
+
21
+ export default function AdminDashboardPage() {
22
+ const { toast } = useToast();
23
+ const [currentApiKey, setCurrentApiKey] = useState("");
24
+ const [showApiKey, setShowApiKey] = useState(false);
25
+ const [isLoadingApiKey, setIsLoadingApiKey] = useState(true);
26
+ const [isUpdatingApiKey, setIsUpdatingApiKey] = useState(false);
27
+
28
+ const [users, setUsers] = useState<User[]>([]);
29
+ const [isLoadingUsers, setIsLoadingUsers] = useState(true);
30
+ const [isRefreshingUsers, setIsRefreshingUsers] = useState(false);
31
+ const [searchTerm, setSearchTerm] = useState("");
32
+
33
+ const [selectedUser, setSelectedUser] = useState<User | null>(null);
34
+ const [isEditUserDialogOpen, setIsEditUserDialogOpen] = useState(false);
35
+
36
+ const apiKeyForm = useForm<PlatformApiKeyInput>({
37
+ resolver: zodResolver(PlatformApiKeySchema),
38
+ defaultValues: { apiKey: "" },
39
+ });
40
+
41
+ const fetchAdminApiKey = useCallback(async () => {
42
+ setIsLoadingApiKey(true);
43
+ try {
44
+ const apiKeyResult = await getPlatformApiKey();
45
+ if (apiKeyResult.success && apiKeyResult.apiKey) {
46
+ setCurrentApiKey(apiKeyResult.apiKey);
47
+ apiKeyForm.setValue("apiKey", apiKeyResult.apiKey);
48
+ } else if (!apiKeyResult.success && apiKeyResult.message) {
49
+ toast({ title: "Error", description: `Failed to load API Key: ${apiKeyResult.message}`, variant: "destructive" });
50
+ }
51
+ } catch (error) {
52
+ toast({ title: "Error", description: "Failed to load API key.", variant: "destructive" });
53
+ } finally {
54
+ setIsLoadingApiKey(false);
55
+ }
56
+ }, [toast, apiKeyForm]);
57
+
58
+ const fetchAllUsers = useCallback(async () => {
59
+ setIsLoadingUsers(true);
60
+ try {
61
+ const usersResult = await getAllUsersForAdmin();
62
+ if (usersResult.success && usersResult.users) {
63
+ setUsers(usersResult.users);
64
+ } else if (!usersResult.success && usersResult.message) {
65
+ toast({ title: "Error", description: `Failed to load users: ${usersResult.message}`, variant: "destructive" });
66
+ }
67
+ } catch (error) {
68
+ toast({ title: "Error", description: "Failed to load users.", variant: "destructive" });
69
+ } finally {
70
+ setIsLoadingUsers(false);
71
+ }
72
+ }, [toast]);
73
+
74
+ const handleRefreshUsers = async () => {
75
+ setIsRefreshingUsers(true);
76
+ await fetchAllUsers();
77
+ setIsRefreshingUsers(false);
78
+ }
79
+
80
+ useEffect(() => {
81
+ fetchAdminApiKey();
82
+ fetchAllUsers();
83
+ }, [fetchAdminApiKey, fetchAllUsers]);
84
+
85
+ const filteredUsers = useMemo(() => {
86
+ if (!searchTerm) return users;
87
+ const lowercasedSearchTerm = searchTerm.toLowerCase();
88
+ return users.filter(user =>
89
+ user.name.toLowerCase().includes(lowercasedSearchTerm) ||
90
+ user.email.toLowerCase().includes(lowercasedSearchTerm)
91
+ );
92
+ }, [users, searchTerm]);
93
+
94
+ async function onApiKeySubmit(values: PlatformApiKeyInput) {
95
+ setIsUpdatingApiKey(true);
96
+ try {
97
+ const result = await updatePlatformApiKey(values);
98
+ toast({
99
+ title: result.success ? "Success" : "Error",
100
+ description: result.message,
101
+ variant: result.success ? "default" : "destructive",
102
+ });
103
+ if (result.success) {
104
+ setCurrentApiKey(values.apiKey);
105
+ }
106
+ } catch (error) {
107
+ toast({ title: "Error", description: "An unexpected error occurred.", variant: "destructive" });
108
+ } finally {
109
+ setIsUpdatingApiKey(false);
110
+ }
111
+ }
112
+
113
+ const handleOpenEditUserDialog = (user: User) => {
114
+ setSelectedUser(user);
115
+ setIsEditUserDialogOpen(true);
116
+ };
117
+
118
+ const handleUserUpdateSuccess = () => {
119
+ setIsEditUserDialogOpen(false);
120
+ setSelectedUser(null);
121
+ fetchAllUsers();
122
+ };
123
+
124
+ return (
125
+ <div className="space-y-8">
126
+ <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
127
+ <div>
128
+ <h1 className="text-3xl font-bold tracking-tight text-foreground flex items-center">
129
+ <Shield className="mr-3 h-8 w-8 text-primary" /> Admin Dashboard
130
+ </h1>
131
+ <p className="text-muted-foreground">Welcome to the admin control panel.</p>
132
+ </div>
133
+ </div>
134
+
135
+ <Card className="shadow-xl hover:shadow-2xl">
136
+ <CardHeader>
137
+ <CardTitle className="text-lg flex items-center"><KeyRound className="mr-2 h-5 w-5 text-primary"/>Platform API Key Management</CardTitle>
138
+ <CardDescription className="text-xs">Configure the global Platform API Key used for deployments.</CardDescription>
139
+ </CardHeader>
140
+ <CardContent>
141
+ {isLoadingApiKey ? (
142
+ <div className="flex items-center justify-center p-4">
143
+ <Loader2 className="h-6 w-6 animate-spin text-primary" />
144
+ <span className="ml-2">Loading API Key...</span>
145
+ </div>
146
+ ) : (
147
+ <Form {...apiKeyForm}>
148
+ <form onSubmit={apiKeyForm.handleSubmit(onApiKeySubmit)} className="space-y-4">
149
+ <FormField
150
+ control={apiKeyForm.control}
151
+ name="apiKey"
152
+ render={({ field }) => (
153
+ <FormItem>
154
+ <FormLabel>Platform API Key</FormLabel>
155
+ <div className="flex items-center gap-2">
156
+ <FormControl>
157
+ <Input type={showApiKey ? "text" : "password"} placeholder="Enter Platform API Key" {...field} />
158
+ </FormControl>
159
+ <Button type="button" variant="outline" size="icon" onClick={() => setShowApiKey(!showApiKey)} aria-label={showApiKey ? "Hide API key" : "Show API key"}>
160
+ {showApiKey ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
161
+ </Button>
162
+ </div>
163
+ <FormDescription className="text-xs">This key is used by the system to interact with the deployment platform.</FormDescription>
164
+ <FormMessage />
165
+ </FormItem>
166
+ )}
167
+ />
168
+ <Button type="submit" disabled={isUpdatingApiKey}>
169
+ {isUpdatingApiKey && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
170
+ Update API Key
171
+ </Button>
172
+ </form>
173
+ </Form>
174
+ )}
175
+ </CardContent>
176
+ </Card>
177
+
178
+ <Card className="shadow-xl hover:shadow-2xl">
179
+ <CardHeader>
180
+ <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
181
+ <div>
182
+ <CardTitle className="text-lg flex items-center"><Users className="mr-2 h-5 w-5 text-primary"/>User Management</CardTitle>
183
+ <CardDescription className="text-xs">View and manage user accounts. Currently showing {filteredUsers.length} of {users.length} user(s).</CardDescription>
184
+ </div>
185
+ <div className="flex items-center gap-2 w-full sm:w-auto">
186
+ <div className="relative flex-grow sm:flex-grow-0">
187
+ <Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
188
+ <Input
189
+ type="search"
190
+ placeholder="Search name or email..."
191
+ value={searchTerm}
192
+ onChange={(e) => setSearchTerm(e.target.value)}
193
+ className="pl-8 w-full sm:w-[200px] lg:w-[250px]"
194
+ />
195
+ </div>
196
+ <Button variant="outline" size="sm" onClick={handleRefreshUsers} disabled={isRefreshingUsers || isLoadingUsers} className="flex-shrink-0">
197
+ {isRefreshingUsers ? <Loader2 className="mr-2 h-4 w-4 animate-spin"/> : <RefreshCcw className="mr-2 h-4 w-4"/>}
198
+ Refresh
199
+ </Button>
200
+ </div>
201
+ </div>
202
+ </CardHeader>
203
+ <CardContent>
204
+ {isLoadingUsers ? (
205
+ <div className="flex items-center justify-center p-4">
206
+ <Loader2 className="h-6 w-6 animate-spin text-primary" />
207
+ <span className="ml-2">Loading users...</span>
208
+ </div>
209
+ ) : filteredUsers.length > 0 ? (
210
+ <ScrollArea className="h-[400px] w-full rounded-md border">
211
+ <Table>
212
+ <TableHeader className="sticky top-0 bg-card z-10">
213
+ <TableRow>
214
+ <TableHead>Name</TableHead>
215
+ <TableHead>Email</TableHead>
216
+ <TableHead>Role</TableHead>
217
+ <TableHead className="text-right">Coins</TableHead>
218
+ <TableHead>Referral Code</TableHead>
219
+ <TableHead>Joined</TableHead>
220
+ <TableHead className="text-center">Actions</TableHead>
221
+ </TableRow>
222
+ </TableHeader>
223
+ <TableBody>
224
+ {filteredUsers.map((user) => (
225
+ <TableRow key={user._id}>
226
+ <TableCell className="font-medium">{user.name}</TableCell>
227
+ <TableCell>{user.email}</TableCell>
228
+ <TableCell><Badge variant={user.role === 'admin' ? 'default' : 'secondary'} className="text-xs">{user.role}</Badge></TableCell>
229
+ <TableCell className="text-right">{user.coins?.toLocaleString() || 0}</TableCell>
230
+ <TableCell>{user.referralCode || 'N/A'}</TableCell>
231
+ <TableCell>{new Date(user.createdAt).toLocaleDateString()}</TableCell>
232
+ <TableCell className="text-center">
233
+ <Button variant="ghost" size="sm" onClick={() => handleOpenEditUserDialog(user)}>
234
+ <Edit className="mr-2 h-4 w-4" /> Edit
235
+ </Button>
236
+ </TableCell>
237
+ </TableRow>
238
+ ))}
239
+ </TableBody>
240
+ </Table>
241
+ </ScrollArea>
242
+ ) : (
243
+ <p className="text-muted-foreground text-center py-4">{searchTerm ? "No users match your search." : "No users found."}</p>
244
+ )}
245
+ </CardContent>
246
+ </Card>
247
+
248
+ {selectedUser && (
249
+ <EditUserDialog
250
+ user={selectedUser}
251
+ isOpen={isEditUserDialogOpen}
252
+ onOpenChange={setIsEditUserDialogOpen}
253
+ onSuccess={handleUserUpdateSuccess}
254
+ />
255
+ )}
256
+
257
+ <Card className="shadow-xl hover:shadow-2xl">
258
+ <CardHeader>
259
+ <CardTitle className="text-lg flex items-center"><FileText className="mr-2 h-5 w-5 text-primary"/>Application Logs</CardTitle>
260
+ <CardDescription className="text-xs">View system-wide application logs (feature coming soon).</CardDescription>
261
+ </CardHeader>
262
+ <CardContent>
263
+ <p className="text-muted-foreground">
264
+ A centralized place to view important system logs and errors will be available here.
265
+ </p>
266
+ </CardContent>
267
+ </Card>
268
+ </div>
269
+ );
270
+ }
src/app/admin/layout.tsx ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import { ReactNode } from "react";
3
+ import Link from "next/link";
4
+ import { Shield, LogOut, Settings, Users, LayoutDashboardIcon, BarChart3 } from "lucide-react";
5
+ import { Button } from "@/components/ui/button";
6
+ import { getLoggedInUser } from "@/lib/actions/auth";
7
+ import { redirect } from 'next/navigation';
8
+
9
+ interface AdminLayoutProps {
10
+ children: ReactNode;
11
+ }
12
+
13
+ export default async function AdminLayout({ children }: AdminLayoutProps) {
14
+ const user = await getLoggedInUser();
15
+
16
+ if (!user || user.role !== 'admin') {
17
+ redirect('/admin/login');
18
+ }
19
+
20
+ return (
21
+ <div className="flex min-h-screen">
22
+ <aside className="w-64 bg-card border-r p-6 flex flex-col">
23
+ <Link href="/admin/dashboard" className="flex items-center mb-8">
24
+ <Shield className="h-8 w-8 text-primary" />
25
+ <span className="ml-2 text-xl font-semibold text-foreground">Admin Panel</span>
26
+ </Link>
27
+ <nav className="flex flex-col space-y-2 flex-1">
28
+ <Button variant="ghost" className="justify-start" asChild>
29
+ <Link href="/admin/dashboard"><LayoutDashboardIcon className="mr-2 h-4 w-4" /> Dashboard</Link>
30
+ </Button>
31
+ <Button variant="ghost" className="justify-start" asChild>
32
+ <Link href="/admin/stats"><BarChart3 className="mr-2 h-4 w-4" /> Statistics</Link>
33
+ </Button>
34
+ <Button variant="ghost" className="justify-start" asChild>
35
+ <Link href="/admin/dashboard"><Users className="mr-2 h-4 w-4" /> User Management</Link>
36
+ </Button>
37
+ <Button variant="ghost" className="justify-start text-muted-foreground" disabled>
38
+ <Settings className="mr-2 h-4 w-4" /> Site Settings
39
+ </Button>
40
+ {/* Add more admin navigation links here */}
41
+ </nav>
42
+ <div className="mt-auto">
43
+ <Button variant="outline" className="w-full justify-start" asChild>
44
+ {/* This should also call a logout function specific to admin if needed */}
45
+ <Link href="/"><LogOut className="mr-2 h-4 w-4" /> Exit Admin</Link>
46
+ </Button>
47
+ </div>
48
+ </aside>
49
+ <main className="flex-1 p-8 bg-secondary">
50
+ {children}
51
+ </main>
52
+ </div>
53
+ );
54
+ }
src/app/admin/login/page.tsx ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { AdminLoginForm } from "@/components/auth/AdminLoginForm";
2
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
3
+ import Link from "next/link";
4
+ import { ShieldAlert } from "lucide-react";
5
+
6
+ export default function AdminLoginPage() {
7
+ return (
8
+ <div className="min-h-screen flex items-center justify-center bg-secondary p-4">
9
+ <Card className="w-full max-w-md shadow-xl">
10
+ <CardHeader className="text-center">
11
+ <Link href="/" className="inline-flex items-center justify-center mb-4">
12
+ <ShieldAlert className="h-8 w-8 text-primary" />
13
+ <span className="ml-2 text-2xl font-semibold text-foreground">Admin Panel</span>
14
+ </Link>
15
+ <CardTitle className="text-2xl">Administrator Access</CardTitle>
16
+ <CardDescription>Enter your admin credentials to manage the application.</CardDescription>
17
+ </CardHeader>
18
+ <CardContent>
19
+ <AdminLoginForm />
20
+ <p className="mt-6 text-center text-sm text-muted-foreground">
21
+ This area is for authorized personnel only.
22
+ </p>
23
+ </CardContent>
24
+ </Card>
25
+ </div>
26
+ );
27
+ }
src/app/admin/stats/page.tsx ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ export const dynamic = 'force-dynamic';
3
+
4
+ import { getPlatformStats } from "@/lib/actions/admin";
5
+ import { StatCard } from "@/components/admin/StatCard";
6
+ import { Users, Package, Coins as CoinsIcon, AlertTriangle, BarChart3 } from "lucide-react"; // Renamed Coins to CoinsIcon
7
+ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
8
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
9
+ import { Separator } from "@/components/ui/separator";
10
+
11
+ export default async function AdminStatsPage() {
12
+ const statsResult = await getPlatformStats();
13
+
14
+ if (!statsResult.success || !statsResult.stats) {
15
+ return (
16
+ <div className="space-y-8">
17
+ <div>
18
+ <h1 className="text-3xl font-bold tracking-tight text-foreground flex items-center">
19
+ <BarChart3 className="mr-3 h-8 w-8 text-primary" /> Platform Statistics
20
+ </h1>
21
+ </div>
22
+ <Alert variant="destructive">
23
+ <AlertTriangle className="h-4 w-4" />
24
+ <AlertTitle>Error Fetching Stats</AlertTitle>
25
+ <AlertDescription>
26
+ {statsResult.message || "Could not load platform statistics. Please try again later."}
27
+ </AlertDescription>
28
+ </Alert>
29
+ </div>
30
+ );
31
+ }
32
+
33
+ const { totalUsers, totalDeployments, totalCoinsInSystem } = statsResult.stats;
34
+
35
+ return (
36
+ <div className="space-y-8">
37
+ <div>
38
+ <h1 className="text-3xl font-bold tracking-tight text-foreground flex items-center">
39
+ <BarChart3 className="mr-3 h-8 w-8 text-primary" /> Platform Statistics
40
+ </h1>
41
+ <p className="text-muted-foreground">Overview of platform activity and usage.</p>
42
+ </div>
43
+
44
+ <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
45
+ <StatCard title="Total Users" value={totalUsers.toLocaleString()} icon={Users} description="Total number of registered users." />
46
+ <StatCard title="Total Deployments" value={totalDeployments.toLocaleString()} icon={Package} description="Total number of deployments created." />
47
+ <StatCard title="Total Coins in System" value={totalCoinsInSystem.toLocaleString()} icon={CoinsIcon} description="Sum of all user coin balances." />
48
+ </div>
49
+
50
+ <Separator />
51
+
52
+ {/* Placeholder for charts */}
53
+ <Card className="shadow-lg">
54
+ <CardHeader>
55
+ <CardTitle className="flex items-center text-xl">
56
+ <BarChart3 className="mr-2 h-5 w-5 text-accent" />
57
+ Usage Trends
58
+ </CardTitle>
59
+ <CardDescription>Visual charts showing platform growth (feature coming soon).</CardDescription>
60
+ </CardHeader>
61
+ <CardContent>
62
+ <Alert>
63
+ <BarChart3 className="h-4 w-4" />
64
+ <AlertTitle>Charts Coming Soon!</AlertTitle>
65
+ <AlertDescription>
66
+ Detailed charts for user registration trends, deployment activity, and more will be available here in a future update.
67
+ </AlertDescription>
68
+ </Alert>
69
+ </CardContent>
70
+ </Card>
71
+ </div>
72
+ );
73
+ }
src/app/api/genkit/[...slug]/route.ts ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ // src/app/api/genkit/[...slug]/route.ts
2
+ import { genkitNextHandler } from '@genkit-ai/next';
3
+ import '@/ai/flows/analyze-deployment-logs'; // Ensure flows are loaded
4
+ export { genkitNextHandler as GET, genkitNextHandler as POST };
src/app/dashboard/buy-coins/page.tsx ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import { CoinPurchaseForm } from "@/components/billing/CoinPurchaseForm";
3
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
4
+ import { CreditCard, AlertTriangle } from "lucide-react";
5
+ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
6
+
7
+ export const dynamic = 'force-dynamic';
8
+
9
+ export default function BuyCoinsPage() {
10
+ return (
11
+ <div className="space-y-8">
12
+ <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
13
+ <div>
14
+ <h1 className="text-3xl font-bold tracking-tight text-foreground flex items-center">
15
+ <CreditCard className="mr-3 h-8 w-8 text-primary" /> Buy Coins
16
+ </h1>
17
+ <p className="text-muted-foreground">Purchase coins to use for deployments and other features.</p>
18
+ </div>
19
+ </div>
20
+
21
+ <Alert variant="destructive" className="shadow-md">
22
+ <AlertTriangle className="h-4 w-4" />
23
+ <AlertTitle>Important: Payment System Notice</AlertTitle>
24
+ <AlertDescription>
25
+ This coin purchase system is currently in a **simulated mode**.
26
+ Actual payment processing and coin crediting upon successful real payment via Paystack/Flutterwave webhooks are not yet fully implemented.
27
+ Do not use real payment details.
28
+ The backend verification of payments is a critical next step.
29
+ </AlertDescription>
30
+ </Alert>
31
+
32
+ <Card className="shadow-xl">
33
+ <CardHeader>
34
+ <CardTitle>Select a Coin Package</CardTitle>
35
+ <CardDescription>Choose the number of coins you&apos;d like to purchase.</CardDescription>
36
+ </CardHeader>
37
+ <CardContent>
38
+ <CoinPurchaseForm />
39
+ </CardContent>
40
+ </Card>
41
+ </div>
42
+ );
43
+ }
src/app/dashboard/deploy/page.tsx ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { DeploymentForm } from "@/components/deployment/DeploymentForm";
2
+
3
+ export const dynamic = 'force-dynamic';
4
+
5
+ export default function NewDeploymentPage() {
6
+ return (
7
+ <div>
8
+ <DeploymentForm />
9
+ </div>
10
+ );
11
+ }
src/app/dashboard/deployments/[id]/page.tsx ADDED
@@ -0,0 +1,281 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ "use client";
3
+
4
+ import { useEffect, useState, useCallback } from 'react';
5
+ import { useParams, useRouter } from 'next/navigation';
6
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
7
+ import { Badge } from '@/components/ui/badge';
8
+ import { Button } from '@/components/ui/button';
9
+ import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
10
+ import { LogDisplay } from '@/components/deployment/LogDisplay';
11
+ import { AiLogAnalyzer } from '@/components/deployment/AiLogAnalyzer';
12
+ import { DeploymentControls } from '@/components/deployment/DeploymentControls';
13
+ import type { Deployment, DeploymentStatus } from '@/lib/types';
14
+ import { ArrowLeft, CheckCircle2, ExternalLink, Hourglass, AlertTriangle, Zap, Info, PowerOff, Settings2, FileText, Brain, RefreshCcw, Edit } from 'lucide-react';
15
+ import { getDeploymentById, getDeploymentLogs } from '@/lib/actions/deployment';
16
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
17
+ import { Skeleton } from '@/components/ui/skeleton';
18
+ import { EditEnvVariablesDialog } from '@/components/deployment/EditEnvVariablesDialog';
19
+
20
+ function getStatusBadgeVariant(status: DeploymentStatus) {
21
+ switch (status) {
22
+ case 'succeeded': return 'default';
23
+ case 'deploying': return 'secondary';
24
+ case 'pending': return 'outline';
25
+ case 'failed': return 'destructive';
26
+ case 'stopped': return 'outline';
27
+ default: return 'outline';
28
+ }
29
+ }
30
+
31
+ function getStatusIcon(status: DeploymentStatus) {
32
+ switch (status) {
33
+ case 'succeeded': return <CheckCircle2 className="h-5 w-5 text-green-500" />;
34
+ case 'deploying': return <Hourglass className="h-5 w-5 text-blue-500 animate-spin" />;
35
+ case 'pending': return <Hourglass className="h-5 w-5 text-yellow-500" />;
36
+ case 'failed': return <AlertTriangle className="h-5 w-5 text-red-500" />;
37
+ case 'stopped': return <PowerOff className="h-5 w-5 text-gray-500" />;
38
+ default: return <Zap className="h-5 w-5 text-muted-foreground" />;
39
+ }
40
+ }
41
+
42
+ const sensitiveKeywords = ['session', 'api', 'key', 'secret', 'token', 'pass', 'password', 'auth', 'credentials', 'mongodb_uri', 'database_url'];
43
+
44
+ const shouldHideValue = (key: string, value: any): boolean => {
45
+ if (typeof value !== 'string' || value.length <= 25) {
46
+ return false;
47
+ }
48
+ const keyLower = key.toLowerCase();
49
+ return sensitiveKeywords.some(keyword => keyLower.includes(keyword));
50
+ };
51
+
52
+ const displayValueHelper = (key: string, value: any): string => {
53
+ if (typeof value === 'boolean') return value ? 'true' : 'false';
54
+ const sValue = String(value);
55
+ if (shouldHideValue(key, sValue)) return `${sValue.substring(0,10)}... (hidden)`;
56
+ if (sValue.length > 100) return `${sValue.substring(0,97)}...`;
57
+ return sValue;
58
+ };
59
+
60
+
61
+ export default function DeploymentDetailPage() {
62
+ const params = useParams();
63
+ const router = useRouter();
64
+ const id = params.id as string;
65
+
66
+ const [deployment, setDeployment] = useState<Deployment | null>(null);
67
+ const [logs, setLogs] = useState<string[]>([]);
68
+ const [isLoading, setIsLoading] = useState(true);
69
+ const [logsLoading, setLogsLoading] = useState(true);
70
+ const [error, setError] = useState<string | null>(null);
71
+ const [isEditEnvDialogOpen, setIsEditEnvDialogOpen] = useState(false);
72
+
73
+ const fetchDeploymentData = useCallback(async (forceRefresh = false) => {
74
+ if (!id) return;
75
+ if (!deployment || forceRefresh) setIsLoading(true);
76
+ setError(null);
77
+ try {
78
+ const deploymentData = await getDeploymentById(id);
79
+ if (deploymentData) {
80
+ setDeployment(deploymentData);
81
+ if (logsLoading || deploymentData.status !== deployment?.status || forceRefresh) {
82
+ setLogsLoading(true);
83
+ const logData = await getDeploymentLogs(deploymentData.id);
84
+ setLogs(logData || []);
85
+ setLogsLoading(false);
86
+ }
87
+ } else {
88
+ setError("Deployment not found.");
89
+ setDeployment(null);
90
+ }
91
+ } catch (err) {
92
+ console.error("Failed to fetch deployment details:", err);
93
+ setError(err instanceof Error ? err.message : "An unexpected error occurred.");
94
+ setDeployment(null);
95
+ } finally {
96
+ if (!deployment || forceRefresh) setIsLoading(false);
97
+ }
98
+ }, [id, deployment, logsLoading]);
99
+
100
+ useEffect(() => {
101
+ if (id) {
102
+ fetchDeploymentData(true); // Initial fetch with full loading indication
103
+ }
104
+ // eslint-disable-next-line react-hooks/exhaustive-deps
105
+ }, [id]); // Only re-run if ID changes
106
+
107
+ useEffect(() => {
108
+ let intervalId: NodeJS.Timeout | undefined = undefined;
109
+ if (deployment && (deployment.status === 'deploying' || deployment.status === 'pending')) {
110
+ intervalId = setInterval(() => {
111
+ fetchDeploymentData(); // No force refresh for interval, let it be gentle
112
+ }, 10000);
113
+ }
114
+ return () => {
115
+ if (intervalId) clearInterval(intervalId);
116
+ }
117
+ }, [deployment, fetchDeploymentData]);
118
+
119
+
120
+ const handleStatusChange = (newStatus: DeploymentStatus) => {
121
+ if (deployment) {
122
+ setDeployment({ ...deployment, status: newStatus });
123
+ if (newStatus !== 'deploying' && newStatus !== 'pending') {
124
+ fetchDeploymentData(true);
125
+ }
126
+ }
127
+ };
128
+
129
+ const handleEnvUpdateSuccess = () => {
130
+ setIsEditEnvDialogOpen(false);
131
+ fetchDeploymentData(true);
132
+ };
133
+
134
+ if (isLoading && !deployment) {
135
+ return (
136
+ <div className="space-y-6">
137
+ <Skeleton className="h-10 w-1/3" />
138
+ <Skeleton className="h-32 w-full" />
139
+ <Skeleton className="h-72 w-full" />
140
+ <Skeleton className="h-72 w-full" />
141
+ </div>
142
+ );
143
+ }
144
+
145
+ if (error) {
146
+ return (
147
+ <Alert variant="destructive" className="shadow-xl">
148
+ <AlertTriangle className="h-4 w-4" />
149
+ <AlertTitle>Error</AlertTitle>
150
+ <AlertDescription>{error}</AlertDescription>
151
+ <Button onClick={() => router.push('/dashboard')} variant="outline" className="mt-4">
152
+ <ArrowLeft className="mr-2 h-4 w-4" /> Back to Dashboard
153
+ </Button>
154
+ </Alert>
155
+ );
156
+ }
157
+
158
+ if (!deployment) {
159
+ return (
160
+ <Alert variant="destructive" className="shadow-xl">
161
+ <AlertTriangle className="h-4 w-4" />
162
+ <AlertTitle>Error</AlertTitle>
163
+ <AlertDescription>Deployment not found. It might have been deleted or the ID is incorrect.</AlertDescription>
164
+ <Button onClick={() => router.push('/dashboard')} variant="outline" className="mt-4">
165
+ <ArrowLeft className="mr-2 h-4 w-4" /> Back to Dashboard
166
+ </Button>
167
+ </Alert>
168
+ );
169
+ }
170
+
171
+ const combinedLogs = logs.join('\n');
172
+
173
+ return (
174
+ <div className="space-y-8">
175
+ <div className="flex justify-between items-center mb-6">
176
+ <Button variant="outline" onClick={() => router.push('/dashboard')} className="shadow hover:shadow-md">
177
+ <ArrowLeft className="mr-2 h-4 w-4" /> Back to Dashboard
178
+ </Button>
179
+ <Button variant="ghost" onClick={() => fetchDeploymentData(true)} title="Refresh Data" className="text-muted-foreground hover:text-primary">
180
+ <RefreshCcw className="mr-2 h-4 w-4" /> Refresh
181
+ </Button>
182
+ </div>
183
+
184
+ <Card className="shadow-xl hover:shadow-2xl">
185
+ <CardHeader>
186
+ <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-2">
187
+ <div>
188
+ <CardTitle className="text-3xl font-bold text-primary">{deployment.appName}</CardTitle>
189
+ <CardDescription className="text-sm text-muted-foreground">Manage your &quot;{deployment.appName}&quot; deployment.</CardDescription>
190
+ </div>
191
+ <Badge variant={getStatusBadgeVariant(deployment.status)} className="text-md capitalize px-4 py-2 flex items-center gap-2 shadow-md">
192
+ {getStatusIcon(deployment.status)}
193
+ {deployment.status}
194
+ </Badge>
195
+ </div>
196
+ </CardHeader>
197
+ <CardContent className="space-y-6">
198
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-4 text-sm">
199
+ <p><strong className="text-foreground/80">ID:</strong> <span className="font-mono text-muted-foreground">{deployment.id}</span></p>
200
+ <p><strong className="text-foreground/80">Created:</strong> {new Date(deployment.createdAt).toLocaleString()}</p>
201
+ <p><strong className="text-foreground/80">Region:</strong> {deployment.region || 'N/A'}</p>
202
+ {deployment.lastDeployedAt && <p><strong className="text-foreground/80">Last Deployed:</strong> {new Date(deployment.lastDeployedAt).toLocaleString()}</p>}
203
+ </div>
204
+ {deployment.url && (
205
+ <Button variant="link" asChild className="p-0 h-auto text-base">
206
+ <a
207
+ href={deployment.url}
208
+ target="_blank"
209
+ rel="noopener noreferrer"
210
+ className="inline-flex items-center text-accent hover:underline font-medium"
211
+ >
212
+ <ExternalLink className="mr-2 h-4 w-4" /> Visit Deployed App
213
+ </a>
214
+ </Button>
215
+ )}
216
+ <DeploymentControls deploymentId={deployment.id} currentStatus={deployment.status} onStatusChange={handleStatusChange} />
217
+ </CardContent>
218
+ </Card>
219
+
220
+ <Tabs defaultValue="logs" className="w-full">
221
+ <TabsList className="grid w-full grid-cols-2 md:grid-cols-3 gap-2 h-auto shadow-md bg-muted p-1 rounded-lg">
222
+ <TabsTrigger value="logs" className="py-2.5 text-sm data-[state=active]:bg-background data-[state=active]:text-primary data-[state=active]:shadow-lg"><FileText className="mr-2 h-4 w-4"/>Logs</TabsTrigger>
223
+ <TabsTrigger value="ai-analyzer" className="py-2.5 text-sm data-[state=active]:bg-background data-[state=active]:text-primary data-[state=active]:shadow-lg"><Brain className="mr-2 h-4 w-4"/>AI Analyzer</TabsTrigger>
224
+ <TabsTrigger value="env" className="py-2.5 text-sm data-[state=active]:bg-background data-[state=active]:text-primary data-[state=active]:shadow-lg"><Settings2 className="mr-2 h-4 w-4"/>Environment</TabsTrigger>
225
+ </TabsList>
226
+ <TabsContent value="logs" className="mt-6">
227
+ <LogDisplay logs={logs} isLoading={logsLoading} />
228
+ </TabsContent>
229
+ <TabsContent value="ai-analyzer" className="mt-6">
230
+ <AiLogAnalyzer initialLogs={logsLoading ? "Loading logs for analysis..." : combinedLogs} />
231
+ </TabsContent>
232
+ <TabsContent value="env" className="mt-6">
233
+ <Card className="shadow-xl">
234
+ <CardHeader className="flex flex-row justify-between items-center">
235
+ <div>
236
+ <CardTitle className="text-xl flex items-center"><Settings2 className="mr-2 h-5 w-5 text-primary"/>Environment Variables</CardTitle>
237
+ <CardDescription className="text-xs text-muted-foreground">Current environment configuration for this deployment.</CardDescription>
238
+ </div>
239
+ <Button variant="outline" size="sm" onClick={() => setIsEditEnvDialogOpen(true)}>
240
+ <Edit className="mr-2 h-4 w-4" /> Edit Variables
241
+ </Button>
242
+ </CardHeader>
243
+ <CardContent>
244
+ {deployment.envVariables && Object.keys(deployment.envVariables).length > 0 ? (
245
+ <div className="space-y-2 text-sm max-h-96 overflow-y-auto bg-muted/50 p-4 rounded-lg shadow-inner border">
246
+ {Object.entries(deployment.envVariables).map(([key, value]) => (
247
+ <div key={key} className="grid grid-cols-3 gap-2 py-1.5 border-b border-border/50 last:border-b-0 font-mono">
248
+ <strong className="text-foreground/80 col-span-1 truncate" title={key}>{key}:</strong>
249
+ <span className="col-span-2 break-all text-foreground/90" title={typeof value === 'string' || typeof value === 'number' ? String(value) : undefined}>
250
+ {displayValueHelper(key, value)}
251
+ </span>
252
+ </div>
253
+ ))}
254
+ </div>
255
+ ) : (
256
+ <Alert className="shadow-sm">
257
+ <Info className="h-4 w-4" />
258
+ <AlertTitle>No Environment Variables</AlertTitle>
259
+ <AlertDescription>
260
+ No specific environment variables were found for this deployment, or they are not set to be displayed.
261
+ </AlertDescription>
262
+ </Alert>
263
+ )}
264
+ </CardContent>
265
+ </Card>
266
+ </TabsContent>
267
+ </Tabs>
268
+ {deployment && (
269
+ <EditEnvVariablesDialog
270
+ isOpen={isEditEnvDialogOpen}
271
+ onOpenChange={setIsEditEnvDialogOpen}
272
+ deployment={deployment}
273
+ onSuccess={handleEnvUpdateSuccess}
274
+ />
275
+ )}
276
+ </div>
277
+ );
278
+ }
279
+
280
+
281
+
src/app/dashboard/layout.tsx ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import { Header } from "@/components/layout/Header";
3
+ import type { ReactNode } from "react";
4
+ import { getLoggedInUser } from "@/lib/actions/auth";
5
+ import { redirect } from 'next/navigation';
6
+
7
+ interface DashboardLayoutProps {
8
+ children: ReactNode;
9
+ }
10
+
11
+ export default async function DashboardLayout({ children }: DashboardLayoutProps) {
12
+ const user = await getLoggedInUser();
13
+
14
+ if (!user) {
15
+ // If no user is logged in (cookie not present or invalid), redirect to login
16
+ // This is a basic check; more robust auth might involve checking session validity
17
+ redirect('/login');
18
+ }
19
+
20
+ return (
21
+ <div className="flex min-h-screen flex-col">
22
+ <Header user={user} />
23
+ <main className="flex-1 container py-8">
24
+ {children}
25
+ </main>
26
+ <footer className="py-6 border-t">
27
+ <div className="container text-center text-sm text-muted-foreground">
28
+ Anita Deploy &copy; {new Date().getFullYear()}
29
+ </div>
30
+ </footer>
31
+ </div>
32
+ );
33
+ }
src/app/dashboard/page.tsx ADDED
@@ -0,0 +1,244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ "use client";
3
+
4
+ import Link from 'next/link';
5
+ import { Button } from '@/components/ui/button';
6
+ import { PlusCircle, LayoutGrid, Gift, Loader2, Coins, Send, RefreshCcw, Info } from 'lucide-react';
7
+ import { DeploymentCard } from '@/components/deployment/DeploymentCard';
8
+ import { getDeployments } from '@/lib/actions/deployment';
9
+ import { claimDailyCoins } from '@/lib/actions/user';
10
+ import { getLoggedInUser, type LoggedInUser } from '@/lib/actions/auth';
11
+ import type { Deployment } from '@/lib/types';
12
+ import { useEffect, useState, useCallback } from 'react';
13
+ import { useToast } from '@/hooks/use-toast';
14
+ import { TransferCoinsDialog } from '@/components/user/TransferCoinsDialog';
15
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
16
+
17
+ export default function DashboardPage() {
18
+ const [deployments, setDeployments] = useState<Deployment[]>([]);
19
+ const [isLoadingDeployments, setIsLoadingDeployments] = useState(true);
20
+ const [user, setUser] = useState<LoggedInUser | null>(null);
21
+ const [currentCoins, setCurrentCoins] = useState<number>(0);
22
+ const [isClaimingCoins, setIsClaimingCoins] = useState(false);
23
+ const [nextClaimInMs, setNextClaimInMs] = useState<number | null>(null);
24
+ const [countdown, setCountdown] = useState<string>("");
25
+ const [isTransferDialogOpen, setIsTransferDialogOpen] = useState(false);
26
+
27
+ const { toast } = useToast();
28
+
29
+ const fetchInitialData = useCallback(async () => {
30
+ setIsLoadingDeployments(true);
31
+ try {
32
+ const [fetchedUser, fetchedDeployments] = await Promise.all([
33
+ getLoggedInUser(),
34
+ getDeployments()
35
+ ]);
36
+ setUser(fetchedUser);
37
+ setCurrentCoins(fetchedUser?.coins ?? 0);
38
+ setDeployments(fetchedDeployments);
39
+
40
+ if (fetchedUser?.lastCoinClaim) {
41
+ const lastClaimTime = new Date(fetchedUser.lastCoinClaim).getTime();
42
+ const cooldownEndTime = lastClaimTime + 24 * 60 * 60 * 1000;
43
+ const now = Date.now();
44
+ if (cooldownEndTime > now) {
45
+ setNextClaimInMs(cooldownEndTime - now);
46
+ } else {
47
+ setNextClaimInMs(0);
48
+ }
49
+ } else {
50
+ setNextClaimInMs(0);
51
+ }
52
+
53
+ } catch (error) {
54
+ toast({ title: "Error", description: "Failed to load dashboard data.", variant: "destructive" });
55
+ } finally {
56
+ setIsLoadingDeployments(false);
57
+ }
58
+ }, [toast]);
59
+
60
+ useEffect(() => {
61
+ fetchInitialData();
62
+ }, [fetchInitialData]);
63
+
64
+ useEffect(() => {
65
+ let intervalId: NodeJS.Timeout;
66
+ if (nextClaimInMs !== null && nextClaimInMs > 0) {
67
+ intervalId = setInterval(() => {
68
+ setNextClaimInMs(prev => {
69
+ if (prev === null || prev <= 1000) {
70
+ clearInterval(intervalId);
71
+ return 0;
72
+ }
73
+ return prev - 1000;
74
+ });
75
+ }, 1000);
76
+ }
77
+ return () => clearInterval(intervalId);
78
+ }, [nextClaimInMs]);
79
+
80
+ useEffect(() => {
81
+ if (nextClaimInMs !== null && nextClaimInMs > 0) {
82
+ const totalSeconds = Math.floor(nextClaimInMs / 1000);
83
+ const hours = Math.floor(totalSeconds / 3600);
84
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
85
+ const seconds = totalSeconds % 60;
86
+
87
+ const formattedHours = String(hours).padStart(2, '0');
88
+ const formattedMinutes = String(minutes).padStart(2, '0');
89
+ const formattedSeconds = String(seconds).padStart(2, '0');
90
+
91
+ setCountdown(`${formattedHours}:${formattedMinutes}:${formattedSeconds}`);
92
+ } else if (nextClaimInMs === 0) {
93
+ setCountdown("Ready to claim!");
94
+ } else {
95
+ setCountdown("");
96
+ }
97
+ }, [nextClaimInMs]);
98
+
99
+
100
+ const handleClaimCoins = async () => {
101
+ setIsClaimingCoins(true);
102
+ try {
103
+ const result = await claimDailyCoins();
104
+ toast({
105
+ title: result.success ? "Success!" : "Oops!",
106
+ description: result.message,
107
+ variant: result.success ? "default" : "destructive",
108
+ });
109
+ if (result.success && result.newBalance !== undefined) {
110
+ setCurrentCoins(result.newBalance);
111
+ fetchInitialData(); // Refetch to update lastCoinClaim and countdown
112
+ } else if (!result.success && result.nextClaimAvailableInMs) {
113
+ setNextClaimInMs(result.nextClaimAvailableInMs);
114
+ }
115
+ } catch (error) {
116
+ toast({ title: "Error", description: "An unexpected error occurred.", variant: "destructive" });
117
+ } finally {
118
+ setIsClaimingCoins(false);
119
+ }
120
+ };
121
+
122
+ const handleTransferSuccess = (newBalance: number) => {
123
+ setCurrentCoins(newBalance);
124
+ fetchInitialData();
125
+ };
126
+
127
+ return (
128
+ <div className="space-y-8">
129
+ <Card className="shadow-xl hover:shadow-2xl border-primary/20">
130
+ <CardHeader className="p-4 sm:p-6 bg-card rounded-t-lg">
131
+ <div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
132
+ <div>
133
+ <CardTitle className="text-2xl sm:text-3xl font-bold tracking-tight text-primary">Your Deployments</CardTitle>
134
+ <CardDescription className="text-muted-foreground mt-1">Manage your Anita-V4 bot deployments.</CardDescription>
135
+ </div>
136
+ <Button asChild size="lg" className="w-full md:w-auto shadow-md hover:shadow-lg" disabled={!user} title="New Deployment">
137
+ <Link href="/dashboard/deploy">
138
+ <PlusCircle className="mr-2 h-5 w-5" /> New Deployment
139
+ </Link>
140
+ </Button>
141
+ </div>
142
+ </CardHeader>
143
+ {user && (
144
+ <CardContent className="p-4 sm:p-6 border-t">
145
+ <div className="flex flex-col sm:flex-row items-center justify-between gap-4 flex-wrap">
146
+ <div className="flex items-center text-lg bg-secondary/50 p-3 rounded-lg shadow-sm border border-border">
147
+ <Coins className="mr-2.5 h-6 w-6 text-yellow-500" />
148
+ <span className="font-bold text-foreground">{currentCoins.toLocaleString()}</span>
149
+ <span className="ml-1.5 text-sm text-muted-foreground">Coins</span>
150
+ </div>
151
+ <div className="flex flex-col sm:flex-row items-center gap-3 flex-wrap">
152
+ <Button
153
+ variant="outline"
154
+ size="sm"
155
+ onClick={() => setIsTransferDialogOpen(true)}
156
+ disabled={!user}
157
+ className="min-w-[150px] shadow-sm hover:shadow-md"
158
+ title="Transfer Coins"
159
+ >
160
+ <Send className="mr-2 h-4 w-4" /> Transfer Coins
161
+ </Button>
162
+ {nextClaimInMs !== null && (
163
+ <div className="flex flex-col items-center sm:items-end">
164
+ <Button
165
+ onClick={handleClaimCoins}
166
+ disabled={isClaimingCoins || (nextClaimInMs !== null && nextClaimInMs > 0)}
167
+ variant={nextClaimInMs === 0 ? "default" : "outline"}
168
+ size="sm"
169
+ className="min-w-[170px] shadow-sm hover:shadow-md"
170
+ title="Claim Daily Coins"
171
+ >
172
+ {isClaimingCoins ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Gift className="mr-2 h-4 w-4" />}
173
+ {nextClaimInMs > 0 ? `Claim in ${countdown}` :
174
+ (countdown === "Ready to claim!" || nextClaimInMs === 0) ? "Claim Daily Coins" :
175
+ isClaimingCoins ? "Claiming..." : "Daily Coins"}
176
+ </Button>
177
+ {user.referralCode && user.referralCode !== 'N/A' && (
178
+ <p className="text-xs text-muted-foreground mt-1.5 text-center sm:text-right">
179
+ Your Code: <span className="font-semibold text-primary">{user.referralCode}</span>
180
+ </p>
181
+ )}
182
+ </div>
183
+ )}
184
+ </div>
185
+ </div>
186
+ </CardContent>
187
+ )}
188
+ </Card>
189
+
190
+ {isLoadingDeployments ? (
191
+ <div className="text-center py-12">
192
+ <Loader2 className="mx-auto h-12 w-12 text-primary animate-spin mb-4" />
193
+ <p className="text-muted-foreground">Loading deployments...</p>
194
+ </div>
195
+ ) : deployments.length > 0 ? (
196
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
197
+ {deployments.map((deployment) => (
198
+ <DeploymentCard key={deployment.id} deployment={deployment} />
199
+ ))}
200
+ </div>
201
+ ) : (
202
+ <Card className="text-center py-12 border-2 border-dashed rounded-lg shadow-sm hover:shadow-md">
203
+ <CardContent className="flex flex-col items-center">
204
+ <LayoutGrid className="mx-auto h-16 w-16 text-muted-foreground mb-6" />
205
+ <h3 className="text-2xl font-semibold text-foreground">No Deployments Yet</h3>
206
+ <p className="text-muted-foreground mt-2 max-w-md mx-auto">
207
+ Ready to launch your Anita-V4 bot? You&apos;ll need some coins to get started.
208
+ Don&apos;t forget to claim your daily coins!
209
+ </p>
210
+ <div className="flex flex-col sm:flex-row items-center justify-center gap-4 mt-8">
211
+ <Button asChild className="mt-0 shadow-md hover:shadow-lg" disabled={!user} title="Create Deployment">
212
+ <Link href="/dashboard/deploy">
213
+ <PlusCircle className="mr-2 h-4 w-4" /> Create Deployment
214
+ </Link>
215
+ </Button>
216
+ {user && nextClaimInMs !== null && (
217
+ <Button
218
+ onClick={handleClaimCoins}
219
+ disabled={isClaimingCoins || (nextClaimInMs !== null && nextClaimInMs > 0)}
220
+ variant={(nextClaimInMs === 0) ? "default" : "outline"}
221
+ className="shadow-sm hover:shadow-md"
222
+ >
223
+ {isClaimingCoins ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Coins className="mr-2 h-4 w-4" />}
224
+ {nextClaimInMs > 0 ? `Claim in ${countdown}` :
225
+ (countdown === "Ready to claim!" || nextClaimInMs === 0) ? "Claim Daily Coins" :
226
+ isClaimingCoins ? "Claiming..." : "Claim Coins"}
227
+ </Button>
228
+ )}
229
+ </div>
230
+ </CardContent>
231
+ </Card>
232
+ )}
233
+
234
+ {user && (
235
+ <TransferCoinsDialog
236
+ isOpen={isTransferDialogOpen}
237
+ onOpenChange={setIsTransferDialogOpen}
238
+ currentUserCoins={currentCoins}
239
+ onTransferSuccess={handleTransferSuccess}
240
+ />
241
+ )}
242
+ </div>
243
+ );
244
+ }
src/app/dashboard/profile/page.tsx ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import { getLoggedInUser } from "@/lib/actions/auth";
3
+ import { redirect } from 'next/navigation';
4
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
5
+ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
6
+ import { Badge } from "@/components/ui/badge";
7
+ import { CopyButton } from "@/components/ui/CopyButton";
8
+ import { UserCircle, Mail, CalendarDays, Coins, Gift, Shield, Users, BarChart3, KeyRound, UserCog, Trash2, AlertTriangle } from "lucide-react";
9
+ import { UpdateProfileForm } from "@/components/user/UpdateProfileForm";
10
+ import { ChangePasswordForm } from "@/components/user/ChangePasswordForm";
11
+ import { DeleteAccountDialog } from "@/components/user/DeleteAccountDialog";
12
+ import { Separator } from "@/components/ui/separator";
13
+ import { Button } from "@/components/ui/button";
14
+
15
+ export const dynamic = 'force-dynamic';
16
+
17
+ export default async function ProfilePage() {
18
+ const user = await getLoggedInUser();
19
+
20
+ if (!user) {
21
+ redirect("/login");
22
+ }
23
+
24
+ const getAvatarFallback = () => {
25
+ if (user?.name) {
26
+ const nameParts = user.name.split(" ");
27
+ if (nameParts.length > 1 && nameParts[0] && nameParts[1]) {
28
+ return nameParts[0][0].toUpperCase() + nameParts[1][0].toUpperCase();
29
+ }
30
+ return user.name.substring(0, 2).toUpperCase();
31
+ }
32
+ return "U";
33
+ };
34
+
35
+ return (
36
+ <div className="space-y-8">
37
+ <div>
38
+ <h1 className="text-3xl font-bold tracking-tight text-foreground flex items-center">
39
+ <UserCircle className="mr-3 h-8 w-8 text-primary" /> Your Profile
40
+ </h1>
41
+ <p className="text-muted-foreground">View and manage your account details and settings.</p>
42
+ </div>
43
+
44
+ <Card className="shadow-xl hover:shadow-2xl">
45
+ <CardHeader className="flex flex-col items-center text-center sm:flex-row sm:text-left sm:items-start gap-4">
46
+ <Avatar className="h-24 w-24 border-2 border-primary shadow-sm">
47
+ <AvatarImage
48
+ src={`https://placehold.co/100x100.png?text=${getAvatarFallback()}`}
49
+ alt={user.name}
50
+ data-ai-hint="user avatar"
51
+ />
52
+ <AvatarFallback className="text-3xl">{getAvatarFallback()}</AvatarFallback>
53
+ </Avatar>
54
+ <div className="flex-1">
55
+ <CardTitle className="text-2xl">{user.name}</CardTitle>
56
+ <CardDescription className="flex items-center mt-1">
57
+ <Mail className="mr-2 h-4 w-4 text-muted-foreground" /> {user.email}
58
+ </CardDescription>
59
+ {user.role === 'admin' && (
60
+ <Badge variant="default" className="mt-2 text-xs">
61
+ <Shield className="mr-1.5 h-3.5 w-3.5" /> Admin
62
+ </Badge>
63
+ )}
64
+ </div>
65
+ </CardHeader>
66
+ <CardContent className="space-y-6 pt-6">
67
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6 text-sm">
68
+ <div className="flex items-center p-4 bg-secondary/60 rounded-lg shadow">
69
+ <Coins className="mr-3 h-6 w-6 text-yellow-500" />
70
+ <div>
71
+ <p className="text-muted-foreground">Coin Balance</p>
72
+ <p className="font-semibold text-lg text-foreground">{user.coins.toLocaleString()} Coins</p>
73
+ </div>
74
+ </div>
75
+ <div className="flex items-center p-4 bg-secondary/60 rounded-lg shadow">
76
+ <CalendarDays className="mr-3 h-6 w-6 text-blue-500" />
77
+ <div>
78
+ <p className="text-muted-foreground">Joined On</p>
79
+ <p className="font-semibold text-foreground">{new Date(user.createdAt).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}</p>
80
+ </div>
81
+ </div>
82
+ </div>
83
+
84
+ {user.referralCode && user.referralCode !== 'N/A' && (
85
+ <Card className="bg-card border shadow-lg">
86
+ <CardHeader>
87
+ <CardTitle className="text-lg flex items-center">
88
+ <Gift className="mr-2 h-5 w-5 text-accent" />
89
+ Your Referral Code
90
+ </CardTitle>
91
+ <CardDescription className="text-xs">Share this code with friends. You earn 10 coins for each successful referral!</CardDescription>
92
+ </CardHeader>
93
+ <CardContent className="flex flex-col sm:flex-row items-center justify-between gap-2 p-4 bg-muted/40 rounded-md">
94
+ <p className="text-lg font-mono font-semibold text-primary break-all">{user.referralCode}</p>
95
+ <CopyButton textToCopy={user.referralCode} buttonText="Copy Code" />
96
+ </CardContent>
97
+ </Card>
98
+ )}
99
+
100
+ <Card className="bg-card border shadow-lg">
101
+ <CardHeader>
102
+ <CardTitle className="text-lg flex items-center">
103
+ <BarChart3 className="mr-2 h-5 w-5 text-green-500" />
104
+ Referral Statistics
105
+ </CardTitle>
106
+ </CardHeader>
107
+ <CardContent className="space-y-3">
108
+ <div className="flex items-center p-4 bg-secondary/60 rounded-lg shadow-sm">
109
+ <Users className="mr-3 h-5 w-5 text-indigo-500" />
110
+ <div>
111
+ <p className="text-muted-foreground">Users Referred</p>
112
+ <p className="font-semibold text-lg text-foreground">{user.referredUsersCount.toLocaleString()}</p>
113
+ </div>
114
+ </div>
115
+ <div className="flex items-center p-4 bg-secondary/60 rounded-lg shadow-sm">
116
+ <Coins className="mr-3 h-5 w-5 text-amber-500" />
117
+ <div>
118
+ <p className="text-muted-foreground">Total Coins from Referrals</p>
119
+ <p className="font-semibold text-lg text-foreground">{user.referralCoinsEarned.toLocaleString()} Coins</p>
120
+ </div>
121
+ </div>
122
+ </CardContent>
123
+ </Card>
124
+ </CardContent>
125
+ </Card>
126
+
127
+ <Separator />
128
+
129
+ <Card className="shadow-xl hover:shadow-2xl">
130
+ <CardHeader>
131
+ <CardTitle className="text-lg flex items-center">
132
+ <UserCog className="mr-2 h-5 w-5 text-primary" />
133
+ Update Profile Information
134
+ </CardTitle>
135
+ <CardDescription className="text-xs">Change your display name.</CardDescription>
136
+ </CardHeader>
137
+ <CardContent>
138
+ <UpdateProfileForm user={user} />
139
+ </CardContent>
140
+ </Card>
141
+
142
+ <Separator />
143
+
144
+ <Card className="shadow-xl hover:shadow-2xl">
145
+ <CardHeader>
146
+ <CardTitle className="text-lg flex items-center">
147
+ <KeyRound className="mr-2 h-5 w-5 text-primary" />
148
+ Change Password
149
+ </CardTitle>
150
+ <CardDescription className="text-xs">Update your account password.</CardDescription>
151
+ </CardHeader>
152
+ <CardContent>
153
+ <ChangePasswordForm />
154
+ </CardContent>
155
+ </Card>
156
+
157
+ <Separator />
158
+
159
+ <Card className="shadow-xl border-destructive hover:shadow-2xl hover:border-destructive/70">
160
+ <CardHeader>
161
+ <CardTitle className="text-lg flex items-center text-destructive">
162
+ <AlertTriangle className="mr-2 h-5 w-5" />
163
+ Danger Zone
164
+ </CardTitle>
165
+ <CardDescription className="text-xs">Manage irreversible account actions.</CardDescription>
166
+ </CardHeader>
167
+ <CardContent>
168
+ <div className="flex flex-col items-start space-y-4">
169
+ <p className="text-sm text-muted-foreground">
170
+ Deleting your account is permanent. All your data, including deployments,
171
+ will be removed and cannot be recovered.
172
+ </p>
173
+ <DeleteAccountDialog>
174
+ <Button variant="destructive">
175
+ <Trash2 className="mr-2 h-4 w-4" />
176
+ Delete My Account
177
+ </Button>
178
+ </DeleteAccountDialog>
179
+ </div>
180
+ </CardContent>
181
+ </Card>
182
+
183
+ </div>
184
+ );
185
+ }
src/app/favicon.ico ADDED

Git LFS Details

  • SHA256: 6b6fc5ea89c3f2a74e97d8112464cd3910b14ece9db8fba8fa887e412e331112
  • Pointer size: 131 Bytes
  • Size of remote file: 435 kB
src/app/globals.css ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ body {
6
+ font-family: var(--font-inter), Arial, Helvetica, sans-serif;
7
+ }
8
+
9
+ @layer base {
10
+ :root {
11
+ --background: 210 40% 98%; /* Lighter, cleaner background */
12
+ --foreground: 222 47% 11%; /* Dark blue for strong contrast */
13
+
14
+ --card: 0 0% 100%; /* Pure White */
15
+ --card-foreground: 222 47% 11%;
16
+
17
+ --popover: 0 0% 100%;
18
+ --popover-foreground: 222 47% 11%;
19
+
20
+ --primary: 220 70% 50%; /* Vibrant, professional Blue */
21
+ --primary-foreground: 0 0% 100%; /* White */
22
+
23
+ --secondary: 210 40% 93%; /* Softer gray for secondary elements */
24
+ --secondary-foreground: 222 30% 25%;
25
+
26
+ --muted: 210 40% 90%; /* Muted gray */
27
+ --muted-foreground: 210 30% 50%;
28
+
29
+ --accent: 260 80% 60%; /* Rich Purple for accents */
30
+ --accent-foreground: 0 0% 100%; /* White */
31
+
32
+ --destructive: 0 75% 55%; /* Clear Red for destructive actions */
33
+ --destructive-foreground: 0 0% 100%;
34
+
35
+ --border: 210 30% 85%;
36
+ --input: 210 30% 95%;
37
+ --ring: 220 70% 55%; /* Primary blue for rings */
38
+
39
+ --radius: 0.75rem; /* Slightly larger radius for a softer look */
40
+
41
+ /* Chart colors */
42
+ --chart-1: 220 70% 50%;
43
+ --chart-2: 160 60% 45%;
44
+ --chart-3: 30 80% 55%;
45
+ --chart-4: 280 65% 60%;
46
+ --chart-5: 340 75% 55%;
47
+
48
+ --sidebar-background: 220 30% 97%;
49
+ --sidebar-foreground: 220 18% 12%;
50
+ --sidebar-primary: 225 75% 60%;
51
+ --sidebar-primary-foreground: 0 0% 100%;
52
+ --sidebar-accent: 250 70% 65%;
53
+ --sidebar-accent-foreground: 0 0% 100%;
54
+ --sidebar-border: 220 25% 90%;
55
+ --sidebar-ring: 225 75% 65%;
56
+ }
57
+
58
+ .dark {
59
+ --background: 222 47% 8%; /* Very dark blue for background */
60
+ --foreground: 210 40% 96%; /* Light gray/off-white for text */
61
+
62
+ --card: 222 40% 12%; /* Darker card for depth */
63
+ --card-foreground: 210 40% 96%;
64
+
65
+ --popover: 222 40% 10%;
66
+ --popover-foreground: 210 40% 96%;
67
+
68
+ --primary: 220 70% 65%; /* Lighter, vibrant Blue for dark mode */
69
+ --primary-foreground: 222 47% 11%; /* Dark text on light primary */
70
+
71
+ --secondary: 222 30% 18%; /* Darker secondary */
72
+ --secondary-foreground: 210 30% 85%;
73
+
74
+ --muted: 222 25% 22%;
75
+ --muted-foreground: 210 25% 70%;
76
+
77
+ --accent: 260 70% 70%; /* Lighter Purple for dark mode */
78
+ --accent-foreground: 0 0% 98%;
79
+
80
+ --destructive: 0 65% 60%; /* Adjusted red for dark */
81
+ --destructive-foreground: 0 0% 98%;
82
+
83
+ --border: 222 25% 28%;
84
+ --input: 222 25% 20%;
85
+ --ring: 220 70% 65%;
86
+
87
+ /* Chart colors for dark theme */
88
+ --chart-1: 220 70% 60%;
89
+ --chart-2: 160 60% 55%;
90
+ --chart-3: 30 80% 65%;
91
+ --chart-4: 280 65% 70%;
92
+ --chart-5: 340 75% 65%;
93
+
94
+ --sidebar-background: 222 40% 12%;
95
+ --sidebar-foreground: 210 40% 96%;
96
+ --sidebar-primary: 220 70% 65%;
97
+ --sidebar-primary-foreground: 222 47% 11%;
98
+ --sidebar-accent: 260 70% 70%;
99
+ --sidebar-accent-foreground: 0 0% 98%;
100
+ --sidebar-border: 222 25% 28%;
101
+ --sidebar-ring: 220 70% 65%;
102
+ }
103
+ }
104
+
105
+ @layer base {
106
+ * {
107
+ @apply border-border;
108
+ }
109
+ body {
110
+ @apply bg-background text-foreground;
111
+ min-height: 100vh;
112
+ display: flex;
113
+ flex-direction: column;
114
+ }
115
+ main {
116
+ flex-grow: 1;
117
+ }
118
+ }
src/app/layout.tsx ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import type { Metadata } from 'next';
3
+ import { Inter } from 'next/font/google';
4
+ import './globals.css';
5
+ import { Toaster } from "@/components/ui/toaster";
6
+ import { cn } from "@/lib/utils";
7
+ import { ThemeProvider } from '@/components/layout/ThemeProvider';
8
+
9
+ const inter = Inter({
10
+ variable: '--font-inter',
11
+ subsets: ['latin'],
12
+ });
13
+
14
+ export const metadata: Metadata = {
15
+ title: 'Anita Deploy - Deploy Anita-V4 with Ease',
16
+ description: 'Effortlessly deploy your Anita-V4 WhatsApp bot to your chosen platform.',
17
+ icons: {
18
+ icon: '/favicon.ico', // Placeholder, ensure you have a favicon
19
+ },
20
+ };
21
+
22
+ export default function RootLayout({
23
+ children,
24
+ }: Readonly<{
25
+ children: React.ReactNode;
26
+ }>) {
27
+ return (
28
+ <html lang="en" suppressHydrationWarning>
29
+ <body className={cn(inter.variable, "font-sans antialiased")}>
30
+ <ThemeProvider
31
+ defaultTheme="light" // Or "dark", "system" needs more handling in ThemeProvider for OS pref
32
+ storageKey="anita-deploy-theme"
33
+ >
34
+ {children}
35
+ <Toaster />
36
+ </ThemeProvider>
37
+ </body>
38
+ </html>
39
+ );
40
+ }
src/app/page.tsx ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import Link from 'next/link';
3
+ import { Button } from '@/components/ui/button';
4
+ import { ArrowRight, Zap, Cog, Terminal, ShieldCheck } from 'lucide-react';
5
+ import Image from 'next/image';
6
+
7
+ export default function LandingPage() {
8
+ return (
9
+ <div className="flex flex-col min-h-screen">
10
+ <header className="px-4 lg:px-6 h-16 flex items-center border-b sticky top-0 bg-background/95 backdrop-blur-md z-50">
11
+ <Link href="/" className="flex items-center justify-center">
12
+ <Zap className="h-6 w-6 text-primary" />
13
+ <span className="ml-2 text-xl font-semibold text-foreground">Anita Deploy</span>
14
+ </Link>
15
+ <nav className="ml-auto flex gap-4 sm:gap-6">
16
+ <Button variant="outline" asChild>
17
+ <Link href="/login">Login</Link>
18
+ </Button>
19
+ <Button asChild>
20
+ <Link href="/register">Sign Up <ArrowRight className="ml-2 h-4 w-4" /></Link>
21
+ </Button>
22
+ </nav>
23
+ </header>
24
+
25
+ <main className="flex-1">
26
+ <section className="w-full py-20 md:py-28 lg:py-36 bg-gradient-to-br from-background via-secondary/70 to-background">
27
+ <div className="container px-4 md:px-6">
28
+ <div className="grid gap-10 lg:grid-cols-[1fr_500px] lg:gap-16 xl:grid-cols-[1fr_600px]">
29
+ <div className="flex flex-col justify-center space-y-8">
30
+ <div className="space-y-4">
31
+ <h1 className="text-5xl font-extrabold tracking-tight sm:text-6xl xl:text-7xl/none bg-clip-text text-transparent bg-gradient-to-r from-primary to-accent">
32
+ Deploy Anita-V4 Bot in Minutes
33
+ </h1>
34
+ <p className="max-w-[600px] text-muted-foreground md:text-xl">
35
+ Anita Deploy simplifies the process of deploying your own Anita-V4 WhatsApp bot.
36
+ Configure your environment, connect to your deployment platform, and go live effortlessly.
37
+ </p>
38
+ </div>
39
+ <div className="flex flex-col gap-3 min-[400px]:flex-row">
40
+ <Button size="lg" asChild className="shadow-lg hover:shadow-primary/30 transform hover:scale-105 transition-all duration-300">
41
+ <Link href="/dashboard">
42
+ Get Started
43
+ <ArrowRight className="ml-2 h-5 w-5" />
44
+ </Link>
45
+ </Button>
46
+ <Button variant="secondary" size="lg" asChild className="shadow-md hover:shadow-lg transform hover:scale-105 transition-all duration-300">
47
+ <Link href="https://github.com/DavidCyrilTech/Anita-V4" target="_blank" rel="noopener noreferrer">
48
+ View on GitHub
49
+ </Link>
50
+ </Button>
51
+ </div>
52
+ </div>
53
+ <Image
54
+ src="https://files.catbox.moe/jd0s4p.jpg"
55
+ alt="Queen Anita V4 Bot"
56
+ data-ai-hint="chatbot ai"
57
+ width={600}
58
+ height={400}
59
+ className="mx-auto aspect-video overflow-hidden rounded-xl object-contain sm:w-full lg:order-last shadow-2xl border-2 border-primary/20"
60
+ priority
61
+ />
62
+ </div>
63
+ </div>
64
+ </section>
65
+
66
+ <section className="w-full py-16 md:py-24 lg:py-32 bg-background">
67
+ <div className="container px-4 md:px-6">
68
+ <div className="flex flex-col items-center justify-center space-y-4 text-center">
69
+ <div className="space-y-2">
70
+ <div className="inline-block rounded-lg bg-primary/10 px-4 py-2 text-sm font-semibold text-primary shadow-sm">Key Features</div>
71
+ <h2 className="text-4xl font-bold tracking-tight sm:text-5xl text-foreground">Everything You Need to Deploy</h2>
72
+ <p className="max-w-[900px] text-muted-foreground md:text-xl/relaxed lg:text-base/relaxed xl:text-xl/relaxed">
73
+ From easy configuration to AI-powered debugging, Anita Deploy provides a seamless experience.
74
+ </p>
75
+ </div>
76
+ </div>
77
+ <div className="mx-auto grid max-w-5xl items-start gap-10 sm:grid-cols-2 md:gap-12 lg:grid-cols-3 lg:gap-16 mt-16">
78
+ <div className="flex flex-col items-center text-center p-8 rounded-xl border bg-card shadow-lg hover:shadow-xl transition-all duration-300 ease-in-out hover:border-primary/50 transform hover:-translate-y-2">
79
+ <div className="p-4 rounded-full bg-primary/10 mb-6 shadow-md">
80
+ <Cog className="h-12 w-12 text-primary" />
81
+ </div>
82
+ <h3 className="text-2xl font-semibold mb-3 text-foreground">Easy Configuration</h3>
83
+ <p className="text-sm text-muted-foreground">
84
+ Simple form to set up all your Anita-V4 environment variables.
85
+ </p>
86
+ </div>
87
+ <div className="flex flex-col items-center text-center p-8 rounded-xl border bg-card shadow-lg hover:shadow-xl transition-all duration-300 ease-in-out hover:border-primary/50 transform hover:-translate-y-2">
88
+ <div className="p-4 rounded-full bg-primary/10 mb-6 shadow-md">
89
+ <ShieldCheck className="h-12 w-12 text-primary" />
90
+ </div>
91
+ <h3 className="text-2xl font-semibold mb-3 text-foreground">Secure Platform</h3>
92
+ <p className="text-sm text-muted-foreground">
93
+ One-click deployment to your chosen platform, with security in mind.
94
+ </p>
95
+ </div>
96
+ <div className="flex flex-col items-center text-center p-8 rounded-xl border bg-card shadow-lg hover:shadow-xl transition-all duration-300 ease-in-out hover:border-primary/50 transform hover:-translate-y-2">
97
+ <div className="p-4 rounded-full bg-primary/10 mb-6 shadow-md">
98
+ <Terminal className="h-12 w-12 text-primary" />
99
+ </div>
100
+ <h3 className="text-2xl font-semibold mb-3 text-foreground">AI-Powered Debugging</h3>
101
+ <p className="text-sm text-muted-foreground">
102
+ Analyze deployment logs with AI to quickly identify and fix issues.
103
+ </p>
104
+ </div>
105
+ </div>
106
+ </div>
107
+ </section>
108
+ </main>
109
+
110
+ <footer className="flex flex-col gap-2 sm:flex-row py-8 w-full shrink-0 items-center px-4 md:px-6 border-t bg-secondary/50">
111
+ <p className="text-xs text-muted-foreground">&copy; {new Date().getFullYear()} Anita Deploy. All rights reserved.</p>
112
+ <nav className="sm:ml-auto flex gap-4 sm:gap-6">
113
+ <Link href="/privacy" className="text-xs hover:underline underline-offset-4 text-muted-foreground hover:text-primary">
114
+ Privacy Policy
115
+ </Link>
116
+ <Link href="/terms" className="text-xs hover:underline underline-offset-4 text-muted-foreground hover:text-primary">
117
+ Terms of Service
118
+ </Link>
119
+ </nav>
120
+ </footer>
121
+ </div>
122
+ );
123
+ }
src/apphosting.yaml ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # Settings to manage and configure a Firebase App Hosting backend.
2
+ # https://firebase.google.com/docs/app-hosting/configure
3
+
4
+ runConfig:
5
+ # Increase this value if you'd like to automatically spin up
6
+ # more instances in response to increased traffic.
7
+ maxInstances: 1
src/components.json ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "default",
4
+ "rsc": true,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "tailwind.config.ts",
8
+ "css": "src/app/globals.css",
9
+ "baseColor": "neutral",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "aliases": {
14
+ "components": "@/components",
15
+ "utils": "@/lib/utils",
16
+ "ui": "@/components/ui",
17
+ "lib": "@/lib",
18
+ "hooks": "@/hooks"
19
+ },
20
+ "iconLibrary": "lucide"
21
+ }
src/components/admin/EditUserDialog.tsx ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ "use client";
3
+
4
+ import {
5
+ Dialog,
6
+ DialogContent,
7
+ DialogHeader,
8
+ DialogTitle,
9
+ DialogDescription,
10
+ DialogFooter,
11
+ DialogClose,
12
+ } from "@/components/ui/dialog";
13
+ import { Button } from "@/components/ui/button";
14
+ import { Input } from "@/components/ui/input";
15
+ import { Label } from "@/components/ui/label";
16
+ import {
17
+ Select,
18
+ SelectContent,
19
+ SelectItem,
20
+ SelectTrigger,
21
+ SelectValue,
22
+ } from "@/components/ui/select";
23
+ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
24
+ import { useForm } from "react-hook-form";
25
+ import { zodResolver } from "@hookform/resolvers/zod";
26
+ import { z } from "zod"; // Added missing import
27
+ import { UpdateUserRoleAdminSchema, UpdateUserCoinsAdminSchema, type UserRole } from "@/lib/schemas";
28
+ import { updateUserRoleAdmin, updateUserCoinsAdmin } from "@/lib/actions/admin";
29
+ import { useToast } from "@/hooks/use-toast";
30
+ import { useState, useEffect } from "react"; // useEffect was imported but not used, keeping it for now in case.
31
+ import { Loader2 } from "lucide-react";
32
+ import type { User } from "@/lib/types";
33
+
34
+ interface EditUserDialogProps {
35
+ user: User | null;
36
+ isOpen: boolean;
37
+ onOpenChange: (isOpen: boolean) => void;
38
+ onSuccess: () => void; // Callback after successful update
39
+ }
40
+
41
+ type EditUserFormData = {
42
+ newRole: UserRole;
43
+ coinAdjustment: number;
44
+ };
45
+
46
+ export function EditUserDialog({ user, isOpen, onOpenChange, onSuccess }: EditUserDialogProps) {
47
+ const { toast } = useToast();
48
+ const [isSubmittingRole, setIsSubmittingRole] = useState(false);
49
+ const [isSubmittingCoins, setIsSubmittingCoins] = useState(false);
50
+
51
+ const form = useForm<EditUserFormData>({
52
+ resolver: zodResolver(
53
+ z.object({ // This is where 'z' was needed
54
+ newRole: UpdateUserRoleAdminSchema.shape.newRole,
55
+ coinAdjustment: UpdateUserCoinsAdminSchema.shape.coinAdjustment,
56
+ })
57
+ ),
58
+ defaultValues: {
59
+ newRole: user?.role || "user",
60
+ coinAdjustment: 0,
61
+ },
62
+ });
63
+
64
+ // Update form defaults when user prop changes
65
+ // Using useEffect to reset form when user prop changes
66
+ useEffect(() => {
67
+ if (user) {
68
+ form.reset({
69
+ newRole: user.role || "user",
70
+ coinAdjustment: 0,
71
+ });
72
+ }
73
+ }, [user, form.reset]);
74
+
75
+
76
+ if (!user) return null;
77
+
78
+ const handleRoleSubmit = async (values: { newRole: UserRole }) => {
79
+ setIsSubmittingRole(true);
80
+ try {
81
+ const result = await updateUserRoleAdmin({ userId: user._id, newRole: values.newRole });
82
+ toast({
83
+ title: result.success ? "Success" : "Error",
84
+ description: result.message,
85
+ variant: result.success ? "default" : "destructive",
86
+ });
87
+ if (result.success) {
88
+ onSuccess();
89
+ }
90
+ } catch (error) {
91
+ toast({ title: "Error", description: "An unexpected error occurred.", variant: "destructive" });
92
+ } finally {
93
+ setIsSubmittingRole(false);
94
+ }
95
+ };
96
+
97
+ const handleCoinsSubmit = async (values: { coinAdjustment: number }) => {
98
+ setIsSubmittingCoins(true);
99
+ try {
100
+ const result = await updateUserCoinsAdmin({ userId: user._id, coinAdjustment: values.coinAdjustment });
101
+ toast({
102
+ title: result.success ? "Success" : "Error",
103
+ description: result.message,
104
+ variant: result.success ? "default" : "destructive",
105
+ });
106
+ if (result.success) {
107
+ form.setValue("coinAdjustment", 0); // Reset coin adjustment field
108
+ onSuccess();
109
+ }
110
+ } catch (error) {
111
+ toast({ title: "Error", description: "An unexpected error occurred.", variant: "destructive" });
112
+ } finally {
113
+ setIsSubmittingCoins(false);
114
+ }
115
+ };
116
+
117
+ return (
118
+ <Dialog open={isOpen} onOpenChange={onOpenChange}>
119
+ <DialogContent className="sm:max-w-[480px]">
120
+ <DialogHeader>
121
+ <DialogTitle>Edit User: {user.name}</DialogTitle>
122
+ <DialogDescription>
123
+ Modify user role and coin balance. Current email: {user.email} (cannot be changed).
124
+ </DialogDescription>
125
+ </DialogHeader>
126
+
127
+ <Form {...form}>
128
+ <div className="space-y-6 py-4">
129
+ {/* Role Management Form */}
130
+ <form onSubmit={form.handleSubmit(data => handleRoleSubmit({ newRole: data.newRole }))} className="space-y-4 p-4 border rounded-md">
131
+ <FormField
132
+ control={form.control}
133
+ name="newRole"
134
+ render={({ field }) => (
135
+ <FormItem>
136
+ <FormLabel>User Role</FormLabel>
137
+ <Select onValueChange={field.onChange} defaultValue={field.value}>
138
+ <FormControl>
139
+ <SelectTrigger>
140
+ <SelectValue placeholder="Select a role" />
141
+ </SelectTrigger>
142
+ </FormControl>
143
+ <SelectContent>
144
+ <SelectItem value="user">User</SelectItem>
145
+ <SelectItem value="admin">Admin</SelectItem>
146
+ </SelectContent>
147
+ </Select>
148
+ <FormMessage />
149
+ </FormItem>
150
+ )}
151
+ />
152
+ <Button type="submit" disabled={isSubmittingRole || user.role === form.getValues("newRole")}>
153
+ {isSubmittingRole && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
154
+ Update Role
155
+ </Button>
156
+ </form>
157
+
158
+ {/* Coin Management Form */}
159
+ <form onSubmit={form.handleSubmit(data => handleCoinsSubmit({ coinAdjustment: data.coinAdjustment }))} className="space-y-4 p-4 border rounded-md">
160
+ <FormField
161
+ control={form.control}
162
+ name="coinAdjustment"
163
+ render={({ field }) => (
164
+ <FormItem>
165
+ <FormLabel>Adjust Coins (Current: {user.coins.toLocaleString()})</FormLabel>
166
+ <FormControl>
167
+ <Input
168
+ type="number"
169
+ placeholder="e.g., 50 or -20"
170
+ {...field}
171
+ onChange={e => field.onChange(parseInt(e.target.value,10) || 0)}
172
+ />
173
+ </FormControl>
174
+ <FormMessage />
175
+ </FormItem>
176
+ )}
177
+ />
178
+ <Button type="submit" disabled={isSubmittingCoins || form.getValues("coinAdjustment") === 0}>
179
+ {isSubmittingCoins && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
180
+ Adjust Coins
181
+ </Button>
182
+ </form>
183
+ </div>
184
+ </Form>
185
+
186
+ <DialogFooter>
187
+ <DialogClose asChild>
188
+ <Button type="button" variant="outline">
189
+ Close
190
+ </Button>
191
+ </DialogClose>
192
+ </DialogFooter>
193
+ </DialogContent>
194
+ </Dialog>
195
+ );
196
+ }
src/components/admin/StatCard.tsx ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
3
+ import type { LucideIcon } from "lucide-react";
4
+
5
+ interface StatCardProps {
6
+ title: string;
7
+ value: string | number;
8
+ icon: LucideIcon;
9
+ description?: string;
10
+ }
11
+
12
+ export function StatCard({ title, value, icon: Icon, description }: StatCardProps) {
13
+ return (
14
+ <Card className="shadow-md hover:shadow-lg transition-shadow">
15
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
16
+ <CardTitle className="text-sm font-medium text-muted-foreground">{title}</CardTitle>
17
+ <Icon className="h-5 w-5 text-primary" />
18
+ </CardHeader>
19
+ <CardContent>
20
+ <div className="text-3xl font-bold text-foreground">{value}</div>
21
+ {description && <p className="text-xs text-muted-foreground pt-1">{description}</p>}
22
+ </CardContent>
23
+ </Card>
24
+ );
25
+ }
src/components/auth/AdminLoginForm.tsx ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ "use client";
3
+
4
+ import { zodResolver } from "@hookform/resolvers/zod";
5
+ import { useForm } from "react-hook-form";
6
+ import { Button } from "@/components/ui/button";
7
+ import {
8
+ Form,
9
+ FormControl,
10
+ FormField,
11
+ FormItem,
12
+ FormLabel,
13
+ FormMessage,
14
+ } from "@/components/ui/form";
15
+ import { Input } from "@/components/ui/input";
16
+ import { AdminLoginSchema, type AdminLoginInput } from "@/lib/schemas";
17
+ import { adminLoginUser } from "@/lib/actions/auth";
18
+ import { useToast } from "@/hooks/use-toast";
19
+ import { useState } from "react";
20
+ import { Loader2, ShieldCheck } from "lucide-react";
21
+ // No longer need useRouter for post-login navigation if server action redirects
22
+
23
+ export function AdminLoginForm() {
24
+ const { toast } = useToast();
25
+ const [isLoading, setIsLoading] = useState(false);
26
+
27
+ const form = useForm<AdminLoginInput>({
28
+ resolver: zodResolver(AdminLoginSchema),
29
+ defaultValues: {
30
+ email: "",
31
+ password: "",
32
+ },
33
+ });
34
+
35
+ async function onSubmit(values: AdminLoginInput) {
36
+ setIsLoading(true);
37
+ try {
38
+ const result = await adminLoginUser(values); // This action will now redirect on success
39
+
40
+ // If the action returns (i.e., did not redirect), it means there was an error.
41
+ if (result && result.success === false) {
42
+ toast({
43
+ title: "Admin Login Failed",
44
+ description: result.message,
45
+ variant: "destructive",
46
+ });
47
+ }
48
+ // Successful login will result in a redirect handled by Next.js.
49
+ // The "Admin Login Successful" toast is removed as the page will change.
50
+
51
+ } catch (error: any) {
52
+ // Server actions that redirect throw a special error that Next.js catches.
53
+ if (error.message?.includes('NEXT_REDIRECT')) {
54
+ // This is an expected error during redirect.
55
+ } else {
56
+ toast({
57
+ title: "Error",
58
+ description: error.message || "An unexpected error occurred. Please try again.",
59
+ variant: "destructive",
60
+ });
61
+ }
62
+ } finally {
63
+ setIsLoading(false);
64
+ }
65
+ }
66
+
67
+ return (
68
+ <Form {...form}>
69
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
70
+ <FormField
71
+ control={form.control}
72
+ name="email"
73
+ render={({ field }) => (
74
+ <FormItem>
75
+ <FormLabel>Admin Email</FormLabel>
76
+ <FormControl>
77
+ <Input placeholder="admin@example.com" {...field} type="email" />
78
+ </FormControl>
79
+ <FormMessage />
80
+ </FormItem>
81
+ )}
82
+ />
83
+ <FormField
84
+ control={form.control}
85
+ name="password"
86
+ render={({ field }) => (
87
+ <FormItem>
88
+ <FormLabel>Admin Password</FormLabel>
89
+ <FormControl>
90
+ <Input type="password" placeholder="••••••••" {...field} />
91
+ </FormControl>
92
+ <FormMessage />
93
+ </FormItem>
94
+ )}
95
+ />
96
+ <Button type="submit" className="w-full" disabled={isLoading}>
97
+ {isLoading ? (
98
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
99
+ ) : (
100
+ <ShieldCheck className="mr-2 h-4 w-4" />
101
+ )}
102
+ Login as Admin
103
+ </Button>
104
+ </form>
105
+ </Form>
106
+ );
107
+ }
src/components/auth/LoginForm.tsx ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ "use client";
3
+
4
+ import { zodResolver } from "@hookform/resolvers/zod";
5
+ import { useForm } from "react-hook-form";
6
+ import { Button } from "@/components/ui/button";
7
+ import {
8
+ Form,
9
+ FormControl,
10
+ FormField,
11
+ FormItem,
12
+ FormLabel,
13
+ FormMessage,
14
+ } from "@/components/ui/form";
15
+ import { Input } from "@/components/ui/input";
16
+ import { LoginSchema, type LoginInput } from "@/lib/schemas";
17
+ import { loginUser } from "@/lib/actions/auth";
18
+ import { useToast } from "@/hooks/use-toast";
19
+ import { useState } from "react";
20
+ import { Loader2 } from "lucide-react";
21
+ // No longer need useRouter for post-login navigation if server action redirects
22
+
23
+ export function LoginForm() {
24
+ const { toast } = useToast();
25
+ const [isLoading, setIsLoading] = useState(false);
26
+
27
+ const form = useForm<LoginInput>({
28
+ resolver: zodResolver(LoginSchema),
29
+ defaultValues: {
30
+ email: "",
31
+ password: "",
32
+ },
33
+ });
34
+
35
+ async function onSubmit(values: LoginInput) {
36
+ setIsLoading(true);
37
+ try {
38
+ const result = await loginUser(values); // This action will now redirect on success
39
+
40
+ // If the action returns (i.e., did not redirect), it means there was an error.
41
+ if (result && result.success === false) {
42
+ toast({
43
+ title: "Login Failed",
44
+ description: result.message,
45
+ variant: "destructive",
46
+ });
47
+ }
48
+ // Successful login will result in a redirect handled by Next.js,
49
+ // so client-side navigation or refresh is no longer needed here.
50
+ // The "Login Successful" toast is removed as the page will change.
51
+
52
+ } catch (error: any) {
53
+ // Server actions that redirect throw a special error that Next.js catches.
54
+ // We should not show this as a user-facing toast.
55
+ if (error.message?.includes('NEXT_REDIRECT')) {
56
+ // This is an expected error during redirect, let Next.js handle it.
57
+ // console.log("Caught NEXT_REDIRECT, letting Next.js handle it.");
58
+ } else {
59
+ toast({
60
+ title: "Error",
61
+ description: error.message || "An unexpected error occurred. Please try again.",
62
+ variant: "destructive",
63
+ });
64
+ }
65
+ } finally {
66
+ setIsLoading(false);
67
+ }
68
+ }
69
+
70
+ return (
71
+ <Form {...form}>
72
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
73
+ <FormField
74
+ control={form.control}
75
+ name="email"
76
+ render={({ field }) => (
77
+ <FormItem>
78
+ <FormLabel>Email</FormLabel>
79
+ <FormControl>
80
+ <Input placeholder="you@example.com" {...field} type="email" />
81
+ </FormControl>
82
+ <FormMessage />
83
+ </FormItem>
84
+ )}
85
+ />
86
+ <FormField
87
+ control={form.control}
88
+ name="password"
89
+ render={({ field }) => (
90
+ <FormItem>
91
+ <FormLabel>Password</FormLabel>
92
+ <FormControl>
93
+ <Input type="password" placeholder="••••••••" {...field} />
94
+ </FormControl>
95
+ <FormMessage />
96
+ </FormItem>
97
+ )}
98
+ />
99
+ <Button type="submit" className="w-full" disabled={isLoading}>
100
+ {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
101
+ Login
102
+ </Button>
103
+ </form>
104
+ </Form>
105
+ );
106
+ }
src/components/auth/RegisterForm.tsx ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ "use client";
3
+
4
+ import { zodResolver } from "@hookform/resolvers/zod";
5
+ import { useForm } from "react-hook-form";
6
+ import { Button } from "@/components/ui/button";
7
+ import {
8
+ Form,
9
+ FormControl,
10
+ FormField,
11
+ FormItem,
12
+ FormLabel,
13
+ FormMessage,
14
+ FormDescription,
15
+ } from "@/components/ui/form";
16
+ import { Input } from "@/components/ui/input";
17
+ import { RegisterSchema, type RegisterInput } from "@/lib/schemas";
18
+ import { registerUser } from "@/lib/actions/auth";
19
+ import { useToast } from "@/hooks/use-toast";
20
+ import { useRouter } from "next/navigation";
21
+ import { useState } from "react";
22
+ import { Loader2 } from "lucide-react";
23
+
24
+ export function RegisterForm() {
25
+ const { toast } = useToast();
26
+ const router = useRouter();
27
+ const [isLoading, setIsLoading] = useState(false);
28
+
29
+ const form = useForm<RegisterInput>({
30
+ resolver: zodResolver(RegisterSchema),
31
+ defaultValues: {
32
+ name: "",
33
+ email: "",
34
+ password: "",
35
+ confirmPassword: "",
36
+ referralCode: "",
37
+ },
38
+ });
39
+
40
+ async function onSubmit(values: RegisterInput) {
41
+ setIsLoading(true);
42
+ try {
43
+ const result = await registerUser(values);
44
+ if (result.success) {
45
+ toast({
46
+ title: "Registration Complete!",
47
+ description: result.message,
48
+ });
49
+ router.push('/login');
50
+ } else {
51
+ toast({
52
+ title: "Registration Failed",
53
+ description: result.message,
54
+ variant: "destructive",
55
+ });
56
+ }
57
+ } catch (error) {
58
+ toast({
59
+ title: "Error",
60
+ description: "An unexpected error occurred. Please try again.",
61
+ variant: "destructive",
62
+ });
63
+ } finally {
64
+ setIsLoading(false);
65
+ }
66
+ }
67
+
68
+ return (
69
+ <Form {...form}>
70
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
71
+ <FormField
72
+ control={form.control}
73
+ name="name"
74
+ render={({ field }) => (
75
+ <FormItem>
76
+ <FormLabel>Full Name</FormLabel>
77
+ <FormControl>
78
+ <Input placeholder="Your Name" {...field} />
79
+ </FormControl>
80
+ <FormMessage />
81
+ </FormItem>
82
+ )}
83
+ />
84
+ <FormField
85
+ control={form.control}
86
+ name="email"
87
+ render={({ field }) => (
88
+ <FormItem>
89
+ <FormLabel>Email</FormLabel>
90
+ <FormControl>
91
+ <Input placeholder="you@example.com" {...field} type="email" />
92
+ </FormControl>
93
+ <FormMessage />
94
+ </FormItem>
95
+ )}
96
+ />
97
+ <FormField
98
+ control={form.control}
99
+ name="password"
100
+ render={({ field }) => (
101
+ <FormItem>
102
+ <FormLabel>Password</FormLabel>
103
+ <FormControl>
104
+ <Input type="password" placeholder="••••••••" {...field} />
105
+ </FormControl>
106
+ <FormMessage />
107
+ </FormItem>
108
+ )}
109
+ />
110
+ <FormField
111
+ control={form.control}
112
+ name="confirmPassword"
113
+ render={({ field }) => (
114
+ <FormItem>
115
+ <FormLabel>Confirm Password</FormLabel>
116
+ <FormControl>
117
+ <Input type="password" placeholder="••••••••" {...field} />
118
+ </FormControl>
119
+ <FormMessage />
120
+ </FormItem>
121
+ )}
122
+ />
123
+ <FormField
124
+ control={form.control}
125
+ name="referralCode"
126
+ render={({ field }) => (
127
+ <FormItem>
128
+ <FormLabel>Referral Code (Optional)</FormLabel>
129
+ <FormControl>
130
+ <Input placeholder="Enter referral code" {...field} />
131
+ </FormControl>
132
+ <FormDescription>If you were referred by someone, enter their code here.</FormDescription>
133
+ <FormMessage />
134
+ </FormItem>
135
+ )}
136
+ />
137
+ <Button type="submit" className="w-full" disabled={isLoading}>
138
+ {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
139
+ Create Account
140
+ </Button>
141
+ </form>
142
+ </Form>
143
+ );
144
+ }
src/components/billing/CoinPurchaseForm.tsx ADDED
@@ -0,0 +1,412 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ "use client";
3
+
4
+ import { zodResolver } from "@hookform/resolvers/zod";
5
+ import { useForm, Controller } from "react-hook-form";
6
+ import { Button } from "@/components/ui/button";
7
+ import {
8
+ Form,
9
+ FormControl,
10
+ FormDescription,
11
+ FormField,
12
+ FormItem,
13
+ FormLabel,
14
+ FormMessage,
15
+ } from "@/components/ui/form";
16
+ import {
17
+ Select,
18
+ SelectContent,
19
+ SelectItem,
20
+ SelectTrigger,
21
+ SelectValue,
22
+ } from "@/components/ui/select";
23
+ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
24
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
25
+ import { CoinPurchaseSchema, type CoinPurchaseInput, CoinPurchasePackageEnum, PaymentGatewayEnum, SupportedCurrencyEnum } from "@/lib/schemas";
26
+ import type { CoinPackageDetails, CurrencyInfo } from "@/lib/types";
27
+ import { useToast } from "@/hooks/use-toast";
28
+ import { useRouter } from "next/navigation";
29
+ import { useState, useEffect } from "react";
30
+ import { Loader2, Coins, CreditCard, ShoppingCart, Lock } from "lucide-react";
31
+ import { usePaystackPayment, PaystackButton, PaystackConsumer } from 'react-paystack';
32
+ import { useFlutterwave, closePaymentModal } from 'flutterwave-react-v3';
33
+ import { initiateCoinPurchase, verifyPaymentAndAwardCoins } from "@/lib/actions/billing";
34
+ import { getLoggedInUser, type LoggedInUser } from "@/lib/actions/auth";
35
+
36
+ const paystackPublicKey = process.env.NEXT_PUBLIC_PAYSTACK_PUBLIC_KEY || '';
37
+ const flutterwavePublicKey = process.env.NEXT_PUBLIC_FLUTTERWAVE_PUBLIC_KEY || '';
38
+
39
+ const COIN_BASE_PRICE_NGN = 10; // 1 Coin = 10 NGN
40
+
41
+ const coinPackages: CoinPackageDetails[] = [
42
+ { id: 'small_50', name: "Small Pack", coins: 50, priceNGN: 50 * COIN_BASE_PRICE_NGN, description: "Get started with 50 coins." },
43
+ { id: 'medium_150', name: "Medium Pack", coins: 150, priceNGN: 150 * COIN_BASE_PRICE_NGN, description: "Most popular: 150 coins." },
44
+ { id: 'large_300', name: "Large Pack", coins: 300, priceNGN: 300 * COIN_BASE_PRICE_NGN, description: "Best value: 300 coins." },
45
+ ];
46
+
47
+ const currencyRatesList: CurrencyInfo[] = [
48
+ { code: 'NGN', symbol: '₦', rate: 1, name: 'Nigerian Naira' },
49
+ { code: 'USD', symbol: '$', rate: 0.00063, name: 'US Dollar' },
50
+ { code: 'GBP', symbol: '£', rate: 0.00050, name: 'British Pound' },
51
+ { code: 'EUR', symbol: '€', rate: 0.00058, name: 'Euro' },
52
+ { code: 'GHS', symbol: 'GH₵', rate: 0.0094, name: 'Ghanaian Cedi' },
53
+ { code: 'KES', symbol: 'KSh', rate: 0.093, name: 'Kenyan Shilling' },
54
+ { code: 'ZAR', symbol: 'R', rate: 0.012, name: 'South African Rand' },
55
+ { code: 'UGX', symbol: 'USh', rate: 2.5, name: 'Ugandan Shilling' },
56
+ { code: 'TZS', symbol: 'TSh', rate: 1.6, name: 'Tanzanian Shilling' },
57
+ { code: 'RWF', symbol: 'RF', rate: 0.82, name: 'Rwandan Franc' },
58
+ { code: 'XOF', symbol: 'CFA', rate: 0.38, name: 'West African CFA franc' },
59
+ { code: 'XAF', symbol: 'FCFA', rate: 0.38, name: 'Central African CFA franc' },
60
+ { code: 'CAD', symbol: 'CA$', rate: 0.00086, name: 'Canadian Dollar' },
61
+ { code: 'EGP', symbol: 'E£', rate: 0.030, name: 'Egyptian Pound' },
62
+ { code: 'GNF', symbol: 'FG', rate: 5.4, name: 'Guinean Franc' },
63
+ { code: 'MAD', symbol: 'MAD', rate: 0.0063, name: 'Moroccan Dirham' },
64
+ { code: 'MWK', symbol: 'MK', rate: 1.1, name: 'Malawian Kwacha' },
65
+ { code: 'SLL', symbol: 'Le', rate: 14.0, name: 'Sierra Leonean Leone (New)'},
66
+ { code: 'STD', symbol: 'Db', rate: 14.0, name: 'São Tomé & Príncipe Dobra (New)' },
67
+ { code: 'ZMW', symbol: 'ZK', rate: 0.017, name: 'Zambian Kwacha' },
68
+ { code: 'CLP', symbol: 'CLP$', rate: 0.58, name: 'Chilean Peso' },
69
+ { code: 'COP', symbol: 'COL$', rate: 2.5, name: 'Colombian Peso' },
70
+ ];
71
+
72
+
73
+ export function CoinPurchaseForm() {
74
+ const { toast } = useToast();
75
+ const router = useRouter();
76
+ const [isLoading, setIsLoading] = useState(false);
77
+ const [currentUser, setCurrentUser] = useState<LoggedInUser | null>(null);
78
+
79
+ useEffect(() => {
80
+ getLoggedInUser().then(setCurrentUser);
81
+ }, []);
82
+
83
+ const form = useForm<CoinPurchaseInput>({
84
+ resolver: zodResolver(CoinPurchaseSchema),
85
+ defaultValues: {
86
+ package: "medium_150",
87
+ currency: "NGN",
88
+ paymentGateway: "paystack",
89
+ email: currentUser?.email || "",
90
+ name: currentUser?.name || "",
91
+ },
92
+ });
93
+
94
+ const selectedPackageId = form.watch("package");
95
+ const selectedCurrencyCode = form.watch("currency");
96
+
97
+ const selectedPkg = coinPackages.find(p => p.id === selectedPackageId) || coinPackages[0];
98
+ const selectedCurrInfo = currencyRatesList.find(c => c.code === selectedCurrencyCode) || currencyRatesList[0];
99
+
100
+ const priceInSelectedCurrency = parseFloat((selectedPkg.priceNGN * selectedCurrInfo.rate).toFixed(2));
101
+
102
+ useEffect(() => {
103
+ form.setValue("amountInSelectedCurrency", priceInSelectedCurrency);
104
+ form.setValue("amountInNGN", selectedPkg.priceNGN);
105
+ form.setValue("coinsToCredit", selectedPkg.coins);
106
+ if (currentUser) {
107
+ form.setValue("email", currentUser.email);
108
+ form.setValue("name", currentUser.name);
109
+ }
110
+ }, [selectedPkg, priceInSelectedCurrency, form, currentUser]);
111
+
112
+
113
+ const handlePaymentSuccess = async (response: any, gateway: 'paystack' | 'flutterwave') => {
114
+ console.log(`${gateway} success response:`, response);
115
+ toast({ title: `${gateway} Payment Submitted (Simulation)`, description: `Ref: ${response.reference || response.transaction_id}. Verifying...` });
116
+ setIsLoading(true);
117
+ // SIMULATE webhook verification for now
118
+ const verificationResult = await verifyPaymentAndAwardCoins(gateway, response.reference || response.transaction_id, response);
119
+ toast({
120
+ title: verificationResult.success ? "Purchase Complete!" : "Verification Issue",
121
+ description: verificationResult.message,
122
+ variant: verificationResult.success ? "default" : "destructive",
123
+ });
124
+ if (verificationResult.success) {
125
+ router.refresh();
126
+ }
127
+ setIsLoading(false);
128
+ };
129
+
130
+ const handlePaymentClose = (gateway: 'paystack' | 'flutterwave') => {
131
+ console.log(`${gateway} payment modal closed.`);
132
+ toast({ title: "Payment Cancelled", description: "The payment process was cancelled.", variant: "default" });
133
+ setIsLoading(false);
134
+ };
135
+
136
+ // Paystack Config
137
+ const paystackConfig = {
138
+ reference: new Date().getTime().toString(),
139
+ email: form.getValues("email"),
140
+ amount: priceInSelectedCurrency * 100, // Amount in kobo
141
+ currency: selectedCurrencyCode,
142
+ publicKey: paystackPublicKey,
143
+ metadata: {
144
+ userId: currentUser?._id,
145
+ packageName: selectedPkg.name,
146
+ coins: selectedPkg.coins,
147
+ custom_fields: [
148
+ {
149
+ display_name: "Package",
150
+ variable_name: "package",
151
+ value: selectedPkg.name
152
+ },
153
+ {
154
+ display_name: "Coins",
155
+ variable_name: "coins",
156
+ value: selectedPkg.coins
157
+ }
158
+ ]
159
+ }
160
+ };
161
+
162
+ // Flutterwave Config
163
+ const flutterwaveConfig = {
164
+ public_key: flutterwavePublicKey,
165
+ tx_ref: new Date().getTime().toString(),
166
+ amount: priceInSelectedCurrency,
167
+ currency: selectedCurrencyCode,
168
+ payment_options: "card,mobilemoney,ussd",
169
+ customer: {
170
+ email: form.getValues("email"),
171
+ name: form.getValues("name") || "Anita Deploy User",
172
+ },
173
+ customizations: {
174
+ title: "Anita Deploy - Coin Purchase",
175
+ description: `Payment for ${selectedPkg.coins} coins`,
176
+ logo: "https://placehold.co/100x100.png?text=AD",
177
+ },
178
+ };
179
+
180
+ const initializePaystackPayment = usePaystackPayment(paystackConfig);
181
+ const handleFlutterwavePayment = useFlutterwave(flutterwaveConfig);
182
+
183
+
184
+ async function onSubmit(values: CoinPurchaseInput) {
185
+ setIsLoading(true);
186
+
187
+ if (!paystackPublicKey || !flutterwavePublicKey) {
188
+ toast({title: "Configuration Error", description: "Payment gateway keys are not set. Please contact support.", variant: "destructive"});
189
+ setIsLoading(false);
190
+ return;
191
+ }
192
+
193
+ if (!currentUser) {
194
+ toast({title: "Error", description: "User not loaded. Please refresh.", variant: "destructive"});
195
+ setIsLoading(false);
196
+ return;
197
+ }
198
+
199
+ const currentPaystackConfig = {
200
+ ...paystackConfig,
201
+ reference: `anitad_${currentUser._id}_${new Date().getTime()}`,
202
+ email: values.email,
203
+ amount: values.amountInSelectedCurrency * 100,
204
+ currency: values.currency,
205
+ metadata: {
206
+ userId: currentUser._id,
207
+ packageName: selectedPkg.name,
208
+ coins: selectedPkg.coins,
209
+ transactionType: "coin_purchase",
210
+ custom_fields: [
211
+ { display_name: "Package", variable_name: "package", value: selectedPkg.name },
212
+ { display_name: "Coins", variable_name: "coins", value: selectedPkg.coins }
213
+ ]
214
+ }
215
+ };
216
+
217
+ const currentFlutterwaveConfig = {
218
+ ...flutterwaveConfig,
219
+ tx_ref: `anitad_${currentUser._id}_${new Date().getTime()}`,
220
+ amount: values.amountInSelectedCurrency,
221
+ currency: values.currency,
222
+ customer: {
223
+ email: values.email,
224
+ name: values.name || "Anita Deploy User",
225
+ },
226
+ meta: {
227
+ userId: currentUser._id,
228
+ packageName: selectedPkg.name,
229
+ coins: selectedPkg.coins,
230
+ transactionType: "coin_purchase"
231
+ }
232
+ };
233
+
234
+ const initResult = await initiateCoinPurchase({
235
+ ...values,
236
+ });
237
+
238
+ if (!initResult.success || !initResult.transactionReference) {
239
+ toast({ title: "Initiation Failed", description: initResult.message, variant: "destructive" });
240
+ setIsLoading(false);
241
+ return;
242
+ }
243
+
244
+ currentPaystackConfig.reference = initResult.transactionReference;
245
+ currentFlutterwaveConfig.tx_ref = initResult.transactionReference;
246
+
247
+ if (values.paymentGateway === "paystack") {
248
+ initializePaystackPayment({
249
+ onSuccess: (response) => handlePaymentSuccess(response, 'paystack'),
250
+ onClose: () => handlePaymentClose('paystack'),
251
+ config: currentPaystackConfig,
252
+ });
253
+ } else if (values.paymentGateway === "flutterwave") {
254
+ handleFlutterwavePayment({
255
+ callback: (response) => {
256
+ handlePaymentSuccess(response, 'flutterwave');
257
+ closePaymentModal();
258
+ },
259
+ onClose: () => handlePaymentClose('flutterwave'),
260
+ });
261
+ }
262
+ }
263
+
264
+
265
+ return (
266
+ <Form {...form}>
267
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
268
+
269
+ <FormField
270
+ control={form.control}
271
+ name="package"
272
+ render={({ field }) => (
273
+ <FormItem className="space-y-3">
274
+ <FormLabel className="text-lg font-semibold">1. Select Coin Package</FormLabel>
275
+ <FormControl>
276
+ <RadioGroup
277
+ onValueChange={field.onChange}
278
+ defaultValue={field.value}
279
+ className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"
280
+ >
281
+ {coinPackages.map((pkg) => (
282
+ <FormItem key={pkg.id} className="flex-1">
283
+ <FormControl>
284
+ <RadioGroupItem value={pkg.id} id={pkg.id} className="sr-only" />
285
+ </FormControl>
286
+ <Label
287
+ htmlFor={pkg.id}
288
+ className={`flex flex-col items-center justify-between rounded-lg border-2 bg-card p-4 hover:bg-accent hover:text-accent-foreground cursor-pointer transition-all
289
+ ${field.value === pkg.id ? "border-primary ring-2 ring-primary shadow-lg" : "border-muted"}`}
290
+ >
291
+ <div className="flex items-center text-xl font-semibold mb-2">
292
+ <Coins className="mr-2 h-6 w-6 text-yellow-500" /> {pkg.coins} Coins
293
+ </div>
294
+ <p className="text-sm font-bold text-primary">{pkg.name}</p>
295
+ <p className="text-xs text-muted-foreground mt-1">{pkg.description}</p>
296
+ <p className="text-lg font-semibold mt-3 text-foreground">
297
+ {currencyRatesList.find(c => c.code === 'NGN')?.symbol}
298
+ {pkg.priceNGN.toLocaleString()}
299
+ </p>
300
+ </Label>
301
+ </FormItem>
302
+ ))}
303
+ </RadioGroup>
304
+ </FormControl>
305
+ <FormMessage />
306
+ </FormItem>
307
+ )}
308
+ />
309
+
310
+ <FormField
311
+ control={form.control}
312
+ name="currency"
313
+ render={({ field }) => (
314
+ <FormItem>
315
+ <FormLabel className="text-lg font-semibold">2. Select Currency</FormLabel>
316
+ <Select onValueChange={field.onChange} defaultValue={field.value}>
317
+ <FormControl>
318
+ <SelectTrigger className="w-full md:w-[280px]">
319
+ <SelectValue placeholder="Select currency" />
320
+ </SelectTrigger>
321
+ </FormControl>
322
+ <SelectContent>
323
+ {currencyRatesList.map((currency) => (
324
+ <SelectItem key={currency.code} value={currency.code}>
325
+ {currency.name} ({currency.symbol})
326
+ </SelectItem>
327
+ ))}
328
+ </SelectContent>
329
+ </Select>
330
+ <FormDescription>
331
+ The price will be converted to your selected currency.
332
+ </FormDescription>
333
+ <FormMessage />
334
+ </FormItem>
335
+ )}
336
+ />
337
+
338
+ <Card className="bg-secondary/50 shadow-md">
339
+ <CardHeader>
340
+ <CardTitle className="text-xl">Order Summary</CardTitle>
341
+ </CardHeader>
342
+ <CardContent className="space-y-2">
343
+ <p className="text-lg">
344
+ <span className="font-medium text-muted-foreground">Package:</span> {selectedPkg.name} ({selectedPkg.coins} Coins)
345
+ </p>
346
+ <p className="text-2xl font-bold text-primary">
347
+ <span className="font-medium text-muted-foreground">Total:</span> {selectedCurrInfo.symbol}
348
+ {priceInSelectedCurrency.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
349
+ <span className="text-sm font-normal text-muted-foreground ml-1"> ({selectedCurrencyCode})</span>
350
+ </p>
351
+ {selectedCurrencyCode !== 'NGN' && (
352
+ <p className="text-sm text-muted-foreground">
353
+ (Approx. {currencyRatesList.find(c => c.code === 'NGN')?.symbol}
354
+ {selectedPkg.priceNGN.toLocaleString()} NGN)
355
+ </p>
356
+ )}
357
+ </CardContent>
358
+ </Card>
359
+
360
+
361
+ <FormField
362
+ control={form.control}
363
+ name="paymentGateway"
364
+ render={({ field }) => (
365
+ <FormItem className="space-y-3">
366
+ <FormLabel className="text-lg font-semibold">3. Select Payment Gateway</FormLabel>
367
+ <FormControl>
368
+ <RadioGroup
369
+ onValueChange={field.onChange}
370
+ defaultValue={field.value}
371
+ className="flex flex-col sm:flex-row gap-4"
372
+ >
373
+ <FormItem className="flex-1">
374
+ <FormControl>
375
+ <RadioGroupItem value="paystack" id="paystack" className="sr-only" />
376
+ </FormControl>
377
+ <Label htmlFor="paystack" className={`flex items-center justify-center rounded-lg border-2 p-4 hover:bg-accent hover:text-accent-foreground cursor-pointer ${field.value === "paystack" ? "border-primary ring-2 ring-primary" : "border-muted"}`}>
378
+ <img src="https://assets.paystack.com/assets/img/logos/paystack-logo-vector-deep-blue.svg" alt="Paystack" className="h-7" data-ai-hint="paystack logo"/>
379
+ </Label>
380
+ </FormItem>
381
+ <FormItem className="flex-1">
382
+ <FormControl>
383
+ <RadioGroupItem value="flutterwave" id="flutterwave" className="sr-only" />
384
+ </FormControl>
385
+ <Label htmlFor="flutterwave" className={`flex items-center justify-center rounded-lg border-2 p-4 hover:bg-accent hover:text-accent-foreground cursor-pointer ${field.value === "flutterwave" ? "border-primary ring-2 ring-primary" : "border-muted"}`}>
386
+ <img src="https://flutterwave.com/images/logo-colored.svg" alt="Flutterwave" className="h-7" data-ai-hint="flutterwave logo"/>
387
+ </Label>
388
+ </FormItem>
389
+ </RadioGroup>
390
+ </FormControl>
391
+ <FormMessage />
392
+ </FormItem>
393
+ )}
394
+ />
395
+
396
+
397
+ <Button type="submit" size="lg" className="w-full text-base py-6" disabled={isLoading}>
398
+ {isLoading ? (
399
+ <Loader2 className="mr-2 h-5 w-5 animate-spin" />
400
+ ) : (
401
+ <ShoppingCart className="mr-2 h-5 w-5" />
402
+ )}
403
+ Proceed to Payment
404
+ </Button>
405
+ <p className="text-xs text-muted-foreground text-center flex items-center justify-center">
406
+ <Lock className="h-3 w-3 mr-1.5"/> Secure payment processing by Paystack & Flutterwave.
407
+ </p>
408
+ </form>
409
+ </Form>
410
+ );
411
+ }
412
+
src/components/deployment/AiLogAnalyzer.tsx ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { Button } from "@/components/ui/button";
5
+ import { Textarea } from "@/components/ui/textarea";
6
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
7
+ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
8
+ import { analyzeDeploymentLogs } from "@/lib/actions/deployment"; // Server Action
9
+ import { Loader2, Sparkles, Lightbulb } from "lucide-react";
10
+ import { ScrollArea } from "../ui/scroll-area";
11
+
12
+ interface AiLogAnalyzerProps {
13
+ initialLogs?: string;
14
+ }
15
+
16
+ export function AiLogAnalyzer({ initialLogs = "" }: AiLogAnalyzerProps) {
17
+ const [logsToAnalyze, setLogsToAnalyze] = useState(initialLogs);
18
+ const [analysisResult, setAnalysisResult] = useState<string | null>(null);
19
+ const [isLoading, setIsLoading] = useState(false);
20
+ const [error, setError] = useState<string | null>(null);
21
+
22
+ const handleAnalyze = async () => {
23
+ if (!logsToAnalyze.trim()) {
24
+ setError("Please paste some logs to analyze.");
25
+ return;
26
+ }
27
+ setIsLoading(true);
28
+ setError(null);
29
+ setAnalysisResult(null);
30
+
31
+ try {
32
+ const result = await analyzeDeploymentLogs(logsToAnalyze);
33
+ if (result.success && result.analysis) {
34
+ setAnalysisResult(result.analysis);
35
+ } else {
36
+ setError(result.error || "Failed to get analysis.");
37
+ }
38
+ } catch (e) {
39
+ setError("An unexpected error occurred during analysis.");
40
+ console.error(e);
41
+ } finally {
42
+ setIsLoading(false);
43
+ }
44
+ };
45
+
46
+ return (
47
+ <Card className="shadow-md">
48
+ <CardHeader>
49
+ <CardTitle className="flex items-center">
50
+ <Sparkles className="mr-2 h-5 w-5 text-accent" />
51
+ AI Log Analyzer
52
+ </CardTitle>
53
+ <CardDescription>
54
+ Paste your deployment logs below and let AI help you find issues and suggest fixes.
55
+ </CardDescription>
56
+ </CardHeader>
57
+ <CardContent className="space-y-4">
58
+ <Textarea
59
+ placeholder="Paste your deployment logs here..."
60
+ value={logsToAnalyze}
61
+ onChange={(e) => setLogsToAnalyze(e.target.value)}
62
+ rows={10}
63
+ className="font-mono text-xs"
64
+ />
65
+ <Button onClick={handleAnalyze} disabled={isLoading || !logsToAnalyze.trim()} className="w-full sm:w-auto">
66
+ {isLoading ? (
67
+ <>
68
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" /> Analyzing...
69
+ </>
70
+ ) : (
71
+ <>
72
+ <Lightbulb className="mr-2 h-4 w-4" /> Analyze Logs
73
+ </>
74
+ )}
75
+ </Button>
76
+
77
+ {error && (
78
+ <Alert variant="destructive">
79
+ <AlertTitle>Error</AlertTitle>
80
+ <AlertDescription>{error}</AlertDescription>
81
+ </Alert>
82
+ )}
83
+
84
+ {analysisResult && (
85
+ <Card className="bg-secondary/50">
86
+ <CardHeader>
87
+ <CardTitle className="flex items-center text-lg">
88
+ <Lightbulb className="mr-2 h-5 w-5 text-primary" />
89
+ Analysis Result
90
+ </CardTitle>
91
+ </CardHeader>
92
+ <CardContent>
93
+ <ScrollArea className="h-[300px] w-full rounded-md p-1">
94
+ <pre className="whitespace-pre-wrap text-sm text-foreground/90">{analysisResult}</pre>
95
+ </ScrollArea>
96
+ </CardContent>
97
+ </Card>
98
+ )}
99
+ </CardContent>
100
+ </Card>
101
+ );
102
+ }
src/components/deployment/DeploymentCard.tsx ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Link from 'next/link';
2
+ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
3
+ import { Button } from '@/components/ui/button';
4
+ import { Badge } from '@/components/ui/badge';
5
+ import type { Deployment, DeploymentStatus } from '@/lib/types';
6
+ import { ExternalLink, Zap, AlertTriangle, CheckCircle2, Hourglass, PowerOff, Layers } from 'lucide-react';
7
+ import { cn } from '@/lib/utils';
8
+
9
+ interface DeploymentCardProps {
10
+ deployment: Deployment;
11
+ }
12
+
13
+ function getStatusBadgeVariant(status: DeploymentStatus) {
14
+ switch (status) {
15
+ case 'succeeded':
16
+ return 'default';
17
+ case 'deploying':
18
+ return 'secondary';
19
+ case 'pending':
20
+ return 'outline';
21
+ case 'failed':
22
+ return 'destructive';
23
+ case 'stopped':
24
+ return 'outline';
25
+ default:
26
+ return 'outline';
27
+ }
28
+ }
29
+
30
+ function getStatusIcon(status: DeploymentStatus) {
31
+ switch (status) {
32
+ case 'succeeded':
33
+ return <CheckCircle2 className="h-5 w-5 text-green-500" />;
34
+ case 'deploying':
35
+ return <Hourglass className="h-5 w-5 text-blue-500 animate-spin" />;
36
+ case 'pending':
37
+ return <Hourglass className="h-5 w-5 text-yellow-500" />;
38
+ case 'failed':
39
+ return <AlertTriangle className="h-5 w-5 text-red-500" />;
40
+ case 'stopped':
41
+ return <PowerOff className="h-5 w-5 text-gray-500" />;
42
+ default:
43
+ return <Zap className="h-5 w-5 text-muted-foreground" />;
44
+ }
45
+ }
46
+
47
+
48
+ export function DeploymentCard({ deployment }: DeploymentCardProps) {
49
+ return (
50
+ <Card className="flex flex-col h-full shadow-md hover:shadow-xl transition-shadow duration-300 ease-in-out">
51
+ <CardHeader className="pb-3">
52
+ <div className="flex justify-between items-start gap-2">
53
+ <div className="flex-1">
54
+ <CardTitle className="text-xl font-semibold text-primary flex items-center">
55
+ <Layers className="mr-2.5 h-5 w-5" />
56
+ {deployment.appName}
57
+ </CardTitle>
58
+ <CardDescription className="text-xs mt-1">Deployed: {new Date(deployment.createdAt).toLocaleDateString()}</CardDescription>
59
+ </div>
60
+ <Badge variant={getStatusBadgeVariant(deployment.status)} className="capitalize flex items-center gap-1.5 px-3 py-1 text-xs shadow-sm">
61
+ {getStatusIcon(deployment.status)}
62
+ <span>{deployment.status}</span>
63
+ </Badge>
64
+ </div>
65
+ </CardHeader>
66
+ <CardContent className="space-y-3 flex-grow">
67
+ <p className="text-sm text-muted-foreground">
68
+ Region: <span className="font-medium text-foreground">{deployment.region || 'N/A'}</span>
69
+ </p>
70
+ {deployment.lastDeployedAt && (
71
+ <p className="text-sm text-muted-foreground">
72
+ Last Activity: <span className="font-medium text-foreground">{new Date(deployment.lastDeployedAt).toLocaleDateString()}</span>
73
+ </p>
74
+ )}
75
+ {deployment.url && (
76
+ <div className="mt-2">
77
+ <a
78
+ href={deployment.url}
79
+ target="_blank"
80
+ rel="noopener noreferrer"
81
+ className="text-sm text-accent hover:underline flex items-center font-medium"
82
+ >
83
+ Visit App <ExternalLink className="ml-1.5 h-4 w-4" />
84
+ </a>
85
+ </div>
86
+ )}
87
+ </CardContent>
88
+ <CardFooter className="flex justify-end pt-4 border-t mt-auto">
89
+ <Button asChild variant="outline" size="sm" className="shadow-sm hover:shadow-md">
90
+ <Link href={`/dashboard/deployments/${deployment.id}`}>Manage</Link>
91
+ </Button>
92
+ </CardFooter>
93
+ </Card>
94
+ );
95
+ }