APRK01 commited on
Commit
c35213b
Β·
1 Parent(s): 1745ca3

Configure cloud database and production pages

Browse files
.DS_Store ADDED
Binary file (6.15 kB). View file
 
.env CHANGED
@@ -3,4 +3,4 @@
3
 
4
  BOT_TOKEN=MTQ3ODk0NjQ3MTg0NzEzMzIwNA.G1l1at.LRhMHhegwR6wIOAPx2bcrtUwP2Y7jY9GtH5z6I
5
  OWNER_ID=1428525831898005575
6
- GUILD_ID=1478938887714902016
 
3
 
4
  BOT_TOKEN=MTQ3ODk0NjQ3MTg0NzEzMzIwNA.G1l1at.LRhMHhegwR6wIOAPx2bcrtUwP2Y7jY9GtH5z6I
5
  OWNER_ID=1428525831898005575
6
+ GUILD_ID=1483135435696898170
WSW/.gitignore ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ # env files (can opt-in for committing if needed)
34
+ .env*
35
+
36
+ # vercel
37
+ .vercel
38
+
39
+ # typescript
40
+ *.tsbuildinfo
41
+ next-env.d.ts
WSW/README.md ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
2
+
3
+ ## Getting Started
4
+
5
+ First, run the development server:
6
+
7
+ ```bash
8
+ npm run dev
9
+ # or
10
+ yarn dev
11
+ # or
12
+ pnpm dev
13
+ # or
14
+ bun dev
15
+ ```
16
+
17
+ Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18
+
19
+ You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20
+
21
+ This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
22
+
23
+ ## Learn More
24
+
25
+ To learn more about Next.js, take a look at the following resources:
26
+
27
+ - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28
+ - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29
+
30
+ You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
31
+
32
+ ## Deploy on Vercel
33
+
34
+ The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35
+
36
+ Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
WSW/eslint.config.mjs ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig, globalIgnores } from "eslint/config";
2
+ import nextVitals from "eslint-config-next/core-web-vitals";
3
+ import nextTs from "eslint-config-next/typescript";
4
+
5
+ const eslintConfig = defineConfig([
6
+ ...nextVitals,
7
+ ...nextTs,
8
+ // Override default ignores of eslint-config-next.
9
+ globalIgnores([
10
+ // Default ignores of eslint-config-next:
11
+ ".next/**",
12
+ "out/**",
13
+ "build/**",
14
+ "next-env.d.ts",
15
+ ]),
16
+ ]);
17
+
18
+ export default eslintConfig;
WSW/next.config.ts ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {
4
+ /* config options here */
5
+ };
6
+
7
+ export default nextConfig;
WSW/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
WSW/package.json ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "wsw-temp",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "eslint"
10
+ },
11
+ "dependencies": {
12
+ "@supabase/supabase-js": "^2.99.2",
13
+ "better-sqlite3": "^12.8.0",
14
+ "framer-motion": "^12.37.0",
15
+ "next": "16.1.6",
16
+ "next-auth": "^4.24.13",
17
+ "react": "19.2.3",
18
+ "react-dom": "19.2.3"
19
+ },
20
+ "devDependencies": {
21
+ "@tailwindcss/postcss": "^4",
22
+ "@types/better-sqlite3": "^7.6.13",
23
+ "@types/node": "^20",
24
+ "@types/react": "^19",
25
+ "@types/react-dom": "^19",
26
+ "eslint": "^9",
27
+ "eslint-config-next": "16.1.6",
28
+ "tailwindcss": "^4",
29
+ "typescript": "^5"
30
+ }
31
+ }
WSW/postcss.config.mjs ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;
WSW/public/file.svg ADDED
WSW/public/globe.svg ADDED
WSW/public/next.svg ADDED
WSW/public/vercel.svg ADDED
WSW/public/window.svg ADDED
WSW/src/app/api/auth/[...nextauth]/route.ts ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import NextAuth, { AuthOptions } from "next-auth";
2
+ import DiscordProvider from "next-auth/providers/discord";
3
+
4
+ export const authOptions: AuthOptions = {
5
+ providers: [
6
+ DiscordProvider({
7
+ clientId: process.env.DISCORD_CLIENT_ID as string,
8
+ clientSecret: process.env.DISCORD_CLIENT_SECRET as string,
9
+ }),
10
+ ],
11
+ callbacks: {
12
+ async session({ session, token }) {
13
+ if (session?.user) {
14
+ // Expose the user's Discord ID to the session
15
+ (session.user as any).id = token.sub;
16
+ }
17
+ return session;
18
+ },
19
+ },
20
+ pages: {
21
+ signIn: '/vip', // Redirect custom sign in if needed
22
+ },
23
+ debug: true,
24
+ };
25
+
26
+ const handler = NextAuth(authOptions);
27
+
28
+ export { handler as GET, handler as POST };
WSW/src/app/api/webhook/sellapp/route.ts ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextResponse } from 'next/server';
2
+ import { supabase } from '@/lib/db';
3
+ import crypto from 'crypto';
4
+
5
+ // The webhook secret from your Sell.app developer dashboard
6
+ const SELLAPP_SECRET = process.env.SELLAPP_WEBHOOK_SECRET || 'dummy_secret';
7
+
8
+ export async function POST(request: Request) {
9
+ try {
10
+ const rawBody = await request.text();
11
+ const headersList = request.headers;
12
+
13
+ // Sell.app uses the standard 'x-sellapp-signature' header.
14
+ const signature = headersList.get('x-sellapp-signature');
15
+
16
+ if (!signature) {
17
+ return NextResponse.json({ error: 'Missing signature' }, { status: 401 });
18
+ }
19
+
20
+ // Verify Sell.app Signature
21
+ const hmac = crypto.createHmac('sha256', SELLAPP_SECRET);
22
+ hmac.update(rawBody);
23
+ const calculatedSignature = hmac.digest('hex');
24
+
25
+ if (signature !== calculatedSignature && process.env.NODE_ENV === 'production') {
26
+ return NextResponse.json({ error: 'Invalid signature' }, { status: 403 });
27
+ }
28
+
29
+ const data = JSON.parse(rawBody);
30
+
31
+ // Safety check: is it an order completion?
32
+ if (data.event !== 'order:paid' && data.event !== 'order:completed') {
33
+ return NextResponse.json({ received: true });
34
+ }
35
+
36
+ const order = data.data;
37
+ const additionalInfo = order.additional_information || [];
38
+
39
+ // We search through the additional_information array for the "discord_id" custom field.
40
+ const discordField = additionalInfo.find((field: any) => field.name?.toLowerCase() === 'discord_id');
41
+ const discordId = discordField ? discordField.value : null;
42
+
43
+ if (!discordId) {
44
+ console.error("Webhook Error: Discord ID was not provided in custom fields.");
45
+ return NextResponse.json({ error: 'Missing Discord ID' }, { status: 400 });
46
+ }
47
+
48
+ // Sell.app typically returns total in major units (e.g., 25.00 instead of 2500)
49
+ const price = parseFloat(order.total_amount || order.total);
50
+ let expiresAt: string | null = null;
51
+ const now = new Date();
52
+
53
+ if (price === 3) {
54
+ now.setDate(now.getDate() + 7);
55
+ expiresAt = now.toISOString();
56
+ } else if (price === 7) {
57
+ now.setDate(now.getDate() + 30);
58
+ expiresAt = now.toISOString();
59
+ } else if (price === 15) {
60
+ now.setFullYear(now.getFullYear() + 1);
61
+ expiresAt = now.toISOString();
62
+ } else if (price === 25) {
63
+ expiresAt = null;
64
+ } else {
65
+ console.warn(`Unrecognized price tier: $${price}. Applying lifetime by default.`);
66
+ expiresAt = null;
67
+ }
68
+
69
+ // Save to database using Supabase
70
+ const { error: upsertError } = await supabase
71
+ .from('vip_users')
72
+ .upsert({
73
+ discord_id: discordId,
74
+ expires_at: expiresAt,
75
+ purchased_at: new Date().toISOString()
76
+ }, {
77
+ onConflict: 'discord_id'
78
+ });
79
+
80
+ if (upsertError) throw upsertError;
81
+
82
+ console.log(`[SELLAPP WEBHOOK] Successfully unlocked VIP for Discord ID: ${discordId}`);
83
+ return NextResponse.json({ success: true });
84
+
85
+ } catch (error) {
86
+ console.error('Webhook processing failed:', error);
87
+ return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
88
+ }
89
+ }
WSW/src/app/cracks/page.tsx ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Link from 'next/link';
2
+ import { getWebDropsByCategory, checkVipStatus } from '@/lib/db';
3
+ import FadeIn from '@/components/FadeIn';
4
+ import AnimatedRow from '@/components/AnimatedRow';
5
+ import { getServerSession } from "next-auth/next";
6
+ import { authOptions } from "@/app/api/auth/[...nextauth]/route";
7
+
8
+ export default async function Cracks() {
9
+ const drops = await getWebDropsByCategory('cracks');
10
+ const session = await getServerSession(authOptions);
11
+ const discordId = session?.user ? (session.user as any).id : null;
12
+ const isVip = discordId ? await checkVipStatus(discordId) : false;
13
+
14
+ return (
15
+ <main className="min-h-[100dvh] bg-transparent text-white selection:bg-white/20 selection:text-white font-sans p-4 md:p-6 lg:p-8 flex flex-col overflow-hidden">
16
+
17
+ {/* Structural Brutalist Header */}
18
+ <header className="flex flex-col md:flex-row justify-between items-start md:items-end mb-8 md:mb-12 border-b-2 border-white pb-6 gap-6 md:gap-0">
19
+ <div>
20
+ <h1 className="text-4xl md:text-6xl font-black tracking-tighter uppercase mb-2">
21
+ Cracks
22
+ </h1>
23
+ <p className="font-mono text-xs md:text-sm tracking-widest text-zinc-400 uppercase">
24
+ [ DIRECTORY . 002 ]
25
+ </p>
26
+ </div>
27
+
28
+ <div className="text-left md:text-right">
29
+ <p className="font-mono text-[10px] md:text-xs tracking-[0.3em] text-white">
30
+ WYVERN SOFTWORKS <span className="text-zinc-600">Β© 2026</span>
31
+ </p>
32
+ <p className="font-mono text-[10px] tracking-widest text-zinc-500 mt-1">
33
+ TOTAL INDEXED: {drops.length}
34
+ </p>
35
+ </div>
36
+ </header>
37
+
38
+ <div className="flex flex-col md:flex-row gap-8 flex-1 min-h-0">
39
+
40
+ {/* Module Sidebar */}
41
+ <nav className="w-full md:w-48 lg:w-64 flex-shrink-0 border-b md:border-b-0 md:border-r border-zinc-900 pr-0 md:pr-8 pb-8 md:pb-0 flex flex-row md:flex-col gap-4 overflow-x-auto md:overflow-visible">
42
+ <Link href="/" className="font-mono text-xs tracking-[0.2em] text-zinc-600 hover:text-white transition-colors uppercase whitespace-nowrap hidden md:block mb-8">
43
+ ← Back Base
44
+ </Link>
45
+ <Link href="/" className="font-mono text-xs tracking-[0.2em] text-zinc-600 hover:text-white transition-colors uppercase whitespace-nowrap block md:hidden">
46
+ [ BACK ]
47
+ </Link>
48
+
49
+ {[
50
+ { id: 'sources', label: 'Sources', active: false },
51
+ { id: 'cracks', label: 'Cracks', active: true },
52
+ { id: 'scripts', label: 'Scripts', active: false },
53
+ { id: 'tools', label: 'Tools', active: false },
54
+ ].map((mod) => (
55
+ <Link
56
+ key={mod.id}
57
+ href={`/${mod.id}`}
58
+ className={`font-mono text-xs tracking-widest uppercase py-2 flex items-center gap-3 whitespace-nowrap group ${
59
+ mod.active ? 'text-white' : 'text-zinc-500 hover:text-zinc-300'
60
+ }`}
61
+ >
62
+ <span className={`w-1.5 h-1.5 rounded-full transition-all ${
63
+ mod.active ? 'bg-white shadow-[0_0_10px_rgba(255,255,255,0.8)]' : 'bg-transparent border border-zinc-700 group-hover:border-zinc-500'
64
+ }`} />
65
+ {mod.label}
66
+ </Link>
67
+ ))}
68
+
69
+ <div className="mt-0 md:mt-12 flex items-center md:items-start ml-4 md:ml-0">
70
+ <Link href="/vip" className="font-mono text-[10px] tracking-[0.2em] px-4 py-2 border border-zinc-800 text-zinc-400 hover:text-black hover:bg-white transition-all uppercase whitespace-nowrap">
71
+ Unlock VIP
72
+ </Link>
73
+ </div>
74
+ </nav>
75
+
76
+ {/* Main Content Area */}
77
+ <div className="flex-1 border border-zinc-800 bg-zinc-950/50 flex flex-col min-h-[500px] md:min-h-0 relative overflow-hidden backdrop-blur-sm">
78
+ <FadeIn>
79
+ {/* Top Edge Decoration Removed */}
80
+
81
+ {/* Ledger Table Header (Desktop) */}
82
+ <div className="hidden md:grid grid-cols-12 gap-4 px-8 py-4 border-b border-zinc-800 font-mono text-[9px] tracking-[0.2em] text-zinc-500 uppercase bg-zinc-900/20">
83
+ <div className="col-span-2">Date</div>
84
+ <div className="col-span-1">ID</div>
85
+ <div className="col-span-4">Identifier</div>
86
+ <div className="col-span-2">Availability</div>
87
+ <div className="col-span-2">Status</div>
88
+ <div className="col-span-1 text-right">Action</div>
89
+ </div>
90
+
91
+ {/* Ledger Content */}
92
+ <div className="flex-1 overflow-y-auto w-full">
93
+ {drops.length === 0 ? (
94
+ <div className="flex-1 overflow-y-auto flex items-center justify-center p-8 h-full">
95
+ <div className="font-mono text-[10px] tracking-[0.2em] text-zinc-600 uppercase text-center border border-zinc-800 border-dashed p-8 w-full max-w-sm">
96
+ [ DIRECTORY EMPTY ]
97
+ </div>
98
+ </div>
99
+ ) : (
100
+ drops.map((drop, i) => (
101
+ <AnimatedRow key={drop.id} drop={drop} index={i} prefix="CRK" isVip={isVip} />
102
+ ))
103
+ )}
104
+ </div>
105
+ </FadeIn>
106
+ </div>
107
+ </div>
108
+
109
+ <footer className="mt-8 pt-6 border-t border-zinc-900 flex justify-start items-center text-[9px] font-mono tracking-[0.2em] text-zinc-600 uppercase">
110
+ <p>A Project by APRK</p>
111
+ </footer>
112
+
113
+ </main>
114
+ );
115
+ }
WSW/src/app/favicon.ico ADDED
WSW/src/app/globals.css ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import "tailwindcss";
2
+
3
+ :root {
4
+ --background: #000000;
5
+ --foreground: #ffffff;
6
+ }
7
+
8
+ body {
9
+ background: var(--background);
10
+ color: var(--foreground);
11
+ }
12
+
13
+ /* Base Grain Animation */
14
+ @keyframes noise {
15
+ 0%, 100% { transform: translate(0, 0); }
16
+ 10% { transform: translate(-5%, -5%); }
17
+ 20% { transform: translate(-10%, 5%); }
18
+ 30% { transform: translate(5%, -10%); }
19
+ 40% { transform: translate(-5%, 15%); }
20
+ 50% { transform: translate(-10%, 5%); }
21
+ 60% { transform: translate(15%, 0); }
22
+ 70% { transform: translate(0, 10%); }
23
+ 80% { transform: translate(-15%, 0); }
24
+ 90% { transform: translate(10%, 5%); }
25
+ }
26
+
27
+ .bg-noise {
28
+ position: fixed;
29
+ top: -50%;
30
+ left: -50%;
31
+ right: -50%;
32
+ bottom: -50%;
33
+ width: 200%;
34
+ height: 200vh;
35
+ background: transparent url('data:image/svg+xml,%3Csvg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"%3E%3Cfilter id="noiseFilter"%3E%3CfeTurbulence type="fractalNoise" baseFrequency="0.85" numOctaves="3" stitchTiles="stitch"/%3E%3C/filter%3E%3Crect width="100%25" height="100%25" filter="url(%23noiseFilter)"/%3E%3C/svg%3E');
36
+ opacity: 0.05;
37
+ pointer-events: none;
38
+ animation: noise 8s steps(10) infinite;
39
+ z-index: 50;
40
+ }
WSW/src/app/layout.tsx ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Metadata } from 'next';
2
+ import { Inter, JetBrains_Mono } from 'next/font/google';
3
+ import './globals.css';
4
+
5
+ const inter = Inter({
6
+ variable: '--font-inter',
7
+ subsets: ['latin'],
8
+ });
9
+
10
+ const jetMono = JetBrains_Mono({
11
+ variable: '--font-jet-mono',
12
+ subsets: ['latin'],
13
+ });
14
+
15
+ export const metadata: Metadata = {
16
+ title: 'Wyvern Softworks',
17
+ description: 'Unlocking Digital Potential. Premium scripts, cracks, and tools.',
18
+ };
19
+
20
+ export default function RootLayout({
21
+ children,
22
+ }: Readonly<{
23
+ children: React.ReactNode;
24
+ }>) {
25
+ return (
26
+ <html lang="en">
27
+ <body
28
+ className={`${inter.variable} ${jetMono.variable} antialiased bg-black text-white min-h-screen selection:bg-cyan-500/30 selection:text-cyan-50 font-sans`}
29
+ >
30
+ <div className="bg-noise"></div>
31
+ {children}
32
+ </body>
33
+ </html>
34
+ );
35
+ }
WSW/src/app/page.tsx ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Link from 'next/link';
2
+
3
+ export default function Home() {
4
+ return (
5
+ <main className="min-h-[100dvh] bg-black text-white selection:bg-white/20 selection:text-white font-sans p-4 md:p-6 lg:p-8 flex flex-col overflow-hidden">
6
+
7
+
8
+
9
+ {/* Strict Architectural Grid Container */}
10
+ <div className="flex-1 w-full grid grid-cols-1 lg:grid-cols-12 gap-px bg-zinc-800 border border-zinc-800 rounded-sm overflow-hidden relative shadow-2xl">
11
+
12
+ {/* Left Side: Massive Branding (8 columns) */}
13
+ <div className="lg:col-span-8 lg:col-start-1 bg-black p-6 sm:p-8 md:p-12 lg:p-16 flex flex-col justify-between relative group min-h-[50vh] lg:min-h-0">
14
+
15
+ <div className="flex-1 flex flex-col justify-center max-w-full overflow-hidden">
16
+ {/* Fluid Typography using clamp and vw to perfectly fit without wrapping/overflowing */}
17
+ <h1 className="font-bold tracking-tighter leading-[0.85] uppercase text-white w-full">
18
+ <span className="block text-[15vw] lg:text-[7vw] xl:text-[8vw] 2xl:text-[9rem]">Wyvern</span>
19
+ <span className="block text-[15vw] lg:text-[7vw] xl:text-[8vw] 2xl:text-[9rem] text-zinc-600">Softworks</span>
20
+ </h1>
21
+
22
+ <p className="font-mono text-[10px] sm:text-xs md:text-sm text-zinc-400 max-w-xl mt-8 md:mt-12 lg:mt-16 leading-relaxed tracking-wide">
23
+ Premium software drops, zero-day scripts, and enterprise cracks. Architected with industrial precision and void of unnecessary aesthetics. Access requires clearance.
24
+ </p>
25
+ </div>
26
+
27
+ <div className="font-mono text-[9px] sm:text-[10px] text-zinc-600 tracking-widest mt-8">
28
+ A PROJECT BY APRK // EST. 2026
29
+ </div>
30
+ </div>
31
+
32
+ {/* Right Side: Navigation Grid (4 columns) */}
33
+ <div className="lg:col-span-4 bg-black flex flex-col lg:h-full lg:overflow-y-auto">
34
+
35
+ <div className="p-4 sm:p-6 md:p-8 border-b border-zinc-800 shrink-0 bg-black sticky top-0 z-10 hidden lg:block">
36
+ <h2 className="font-mono text-[9px] sm:text-[10px] tracking-[0.3em] text-zinc-500 uppercase">
37
+ System Modules
38
+ </h2>
39
+ </div>
40
+
41
+ <div className="flex-1 flex flex-col justify-center lg:justify-start">
42
+ <Link href="/sources" className="flex-1 sm:min-h-[80px] p-4 sm:p-6 md:p-8 border-b border-zinc-800 flex items-center justify-between group hover:bg-white hover:text-black transition-colors duration-300">
43
+ <span className="font-mono text-[10px] sm:text-xs md:text-sm tracking-widest uppercase">Sources</span>
44
+ <span className="font-sans text-zinc-500 group-hover:text-black transition-colors">β†—</span>
45
+ </Link>
46
+
47
+ <Link href="/cracks" className="flex-1 sm:min-h-[80px] p-4 sm:p-6 md:p-8 border-b border-zinc-800 flex items-center justify-between group hover:bg-white hover:text-black transition-colors duration-300">
48
+ <span className="font-mono text-[10px] sm:text-xs md:text-sm tracking-widest uppercase">Cracks</span>
49
+ <span className="font-sans text-zinc-500 group-hover:text-black transition-colors">β†—</span>
50
+ </Link>
51
+
52
+ <Link href="/scripts" className="flex-1 sm:min-h-[80px] p-4 sm:p-6 md:p-8 border-b border-zinc-800 flex items-center justify-between group hover:bg-white hover:text-black transition-colors duration-300">
53
+ <span className="font-mono text-[10px] sm:text-xs md:text-sm tracking-widest uppercase">Scripts</span>
54
+ <span className="font-sans text-zinc-500 group-hover:text-black transition-colors">β†—</span>
55
+ </Link>
56
+
57
+ <Link href="/tools" className="flex-1 sm:min-h-[80px] p-4 sm:p-6 md:p-8 lg:border-b border-zinc-800 flex items-center justify-between group hover:bg-white hover:text-black transition-colors duration-300">
58
+ <span className="font-mono text-[10px] sm:text-xs md:text-sm tracking-widest uppercase">Tools</span>
59
+ <span className="font-sans text-zinc-500 group-hover:text-black transition-colors">β†—</span>
60
+ </Link>
61
+
62
+ <Link href="/vip" className="flex-1 sm:min-h-[80px] p-4 sm:p-6 md:p-8 bg-zinc-900 flex items-center justify-between group hover:bg-white hover:text-black transition-colors duration-300">
63
+ <span className="font-mono text-[10px] sm:text-xs md:text-sm tracking-widest uppercase text-white group-hover:text-black">VIP Access</span>
64
+ <span className="font-sans text-white group-hover:text-black">β†—</span>
65
+ </Link>
66
+ </div>
67
+
68
+ </div>
69
+
70
+ </div>
71
+
72
+ </main>
73
+ );
74
+ }
WSW/src/app/scripts/page.tsx ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Link from 'next/link';
2
+ import { getWebDropsByCategory, checkVipStatus } from '@/lib/db';
3
+ import FadeIn from '@/components/FadeIn';
4
+ import AnimatedRow from '@/components/AnimatedRow';
5
+ import { getServerSession } from "next-auth/next";
6
+ import { authOptions } from "@/app/api/auth/[...nextauth]/route";
7
+
8
+ export default async function Scripts() {
9
+ const drops = await getWebDropsByCategory('scripts');
10
+ const session = await getServerSession(authOptions);
11
+ const discordId = session?.user ? (session.user as any).id : null;
12
+ const isVip = discordId ? await checkVipStatus(discordId) : false;
13
+
14
+ return (
15
+ <main className="min-h-[100dvh] bg-transparent text-white selection:bg-white/20 selection:text-white font-sans p-4 md:p-6 lg:p-8 flex flex-col overflow-hidden">
16
+
17
+ {/* Structural Brutalist Header */}
18
+ <header className="flex flex-col md:flex-row justify-between items-start md:items-end mb-8 md:mb-12 border-b-2 border-white pb-6 gap-6 md:gap-0">
19
+ <div>
20
+ <h1 className="text-4xl md:text-6xl font-black tracking-tighter uppercase mb-2">
21
+ Scripts
22
+ </h1>
23
+ <p className="font-mono text-xs md:text-sm tracking-widest text-zinc-400 uppercase">
24
+ [ DIRECTORY . 003 ]
25
+ </p>
26
+ </div>
27
+
28
+ <div className="text-left md:text-right">
29
+ <p className="font-mono text-[10px] md:text-xs tracking-[0.3em] text-white">
30
+ WYVERN SOFTWORKS <span className="text-zinc-600">Β© 2026</span>
31
+ </p>
32
+ <p className="font-mono text-[10px] tracking-widest text-zinc-500 mt-1">
33
+ TOTAL INDEXED: {drops.length}
34
+ </p>
35
+ </div>
36
+ </header>
37
+
38
+ <div className="flex flex-col md:flex-row gap-8 flex-1 min-h-0">
39
+
40
+ {/* Module Sidebar */}
41
+ <nav className="w-full md:w-48 lg:w-64 flex-shrink-0 border-b md:border-b-0 md:border-r border-zinc-900 pr-0 md:pr-8 pb-8 md:pb-0 flex flex-row md:flex-col gap-4 overflow-x-auto md:overflow-visible">
42
+ <Link href="/" className="font-mono text-xs tracking-[0.2em] text-zinc-600 hover:text-white transition-colors uppercase whitespace-nowrap hidden md:block mb-8">
43
+ ← Back Base
44
+ </Link>
45
+ <Link href="/" className="font-mono text-xs tracking-[0.2em] text-zinc-600 hover:text-white transition-colors uppercase whitespace-nowrap block md:hidden">
46
+ [ BACK ]
47
+ </Link>
48
+
49
+ {[
50
+ { id: 'sources', label: 'Sources', active: false },
51
+ { id: 'cracks', label: 'Cracks', active: false },
52
+ { id: 'scripts', label: 'Scripts', active: true },
53
+ { id: 'tools', label: 'Tools', active: false },
54
+ ].map((mod) => (
55
+ <Link
56
+ key={mod.id}
57
+ href={`/${mod.id}`}
58
+ className={`font-mono text-xs tracking-widest uppercase py-2 flex items-center gap-3 whitespace-nowrap group ${
59
+ mod.active ? 'text-white' : 'text-zinc-500 hover:text-zinc-300'
60
+ }`}
61
+ >
62
+ <span className={`w-1.5 h-1.5 rounded-full transition-all ${
63
+ mod.active ? 'bg-white shadow-[0_0_10px_rgba(255,255,255,0.8)]' : 'bg-transparent border border-zinc-700 group-hover:border-zinc-500'
64
+ }`} />
65
+ {mod.label}
66
+ </Link>
67
+ ))}
68
+
69
+ <div className="mt-0 md:mt-12 flex items-center md:items-start ml-4 md:ml-0">
70
+ <Link href="/vip" className="font-mono text-[10px] tracking-[0.2em] px-4 py-2 border border-zinc-800 text-zinc-400 hover:text-black hover:bg-white transition-all uppercase whitespace-nowrap">
71
+ Unlock VIP
72
+ </Link>
73
+ </div>
74
+ </nav>
75
+
76
+ {/* Main Content Area */}
77
+ <div className="flex-1 border border-zinc-800 bg-zinc-950/50 flex flex-col min-h-[500px] md:min-h-0 relative overflow-hidden backdrop-blur-sm">
78
+ <FadeIn>
79
+ {/* Top Edge Decoration Removed */}
80
+
81
+ {/* Ledger Table Header (Desktop) */}
82
+ <div className="hidden md:grid grid-cols-12 gap-4 px-8 py-4 border-b border-zinc-800 font-mono text-[9px] tracking-[0.2em] text-zinc-500 uppercase bg-zinc-900/20">
83
+ <div className="col-span-2">Date</div>
84
+ <div className="col-span-1">ID</div>
85
+ <div className="col-span-4">Identifier</div>
86
+ <div className="col-span-2">Availability</div>
87
+ <div className="col-span-2">Status</div>
88
+ <div className="col-span-1 text-right">Action</div>
89
+ </div>
90
+
91
+ {/* Ledger Content */}
92
+ <div className="flex-1 overflow-y-auto w-full">
93
+ {drops.length === 0 ? (
94
+ <div className="flex-1 overflow-y-auto flex items-center justify-center p-8 h-full">
95
+ <div className="font-mono text-[10px] tracking-[0.2em] text-zinc-600 uppercase text-center border border-zinc-800 border-dashed p-8 w-full max-w-sm">
96
+ [ DIRECTORY EMPTY ]
97
+ </div>
98
+ </div>
99
+ ) : (
100
+ drops.map((drop, i) => (
101
+ <AnimatedRow key={drop.id} drop={drop} index={i} prefix="SCR" isVip={isVip} />
102
+ ))
103
+ )}
104
+ </div>
105
+ </FadeIn>
106
+ </div>
107
+ </div>
108
+
109
+ <footer className="mt-8 pt-6 border-t border-zinc-900 flex justify-start items-center text-[9px] font-mono tracking-[0.2em] text-zinc-600 uppercase">
110
+ <p>A Project by APRK</p>
111
+ </footer>
112
+
113
+ </main>
114
+ );
115
+ }
WSW/src/app/sources/page.tsx ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Link from 'next/link';
2
+ import { getWebDropsByCategory, checkVipStatus } from '@/lib/db';
3
+ import FadeIn from '@/components/FadeIn';
4
+ import AnimatedRow from '@/components/AnimatedRow';
5
+ import { getServerSession } from "next-auth/next";
6
+ import { authOptions } from "@/app/api/auth/[...nextauth]/route";
7
+
8
+ export default async function Sources() {
9
+ const drops = await getWebDropsByCategory('sources');
10
+ const session = await getServerSession(authOptions);
11
+ const discordId = session?.user ? (session.user as any).id : null;
12
+ const isVip = discordId ? await checkVipStatus(discordId) : false;
13
+
14
+ return (
15
+ <main className="min-h-[100dvh] bg-transparent text-white selection:bg-white/20 selection:text-white font-sans p-4 md:p-6 lg:p-8 flex flex-col overflow-hidden">
16
+
17
+ {/* Structural Brutalist Header */}
18
+ <header className="flex flex-col md:flex-row justify-between items-start md:items-end mb-8 md:mb-12 border-b-2 border-white pb-6 gap-6 md:gap-0">
19
+ <div>
20
+ <h1 className="text-4xl md:text-6xl font-black tracking-tighter uppercase mb-2">
21
+ Sources
22
+ </h1>
23
+ <p className="font-mono text-xs md:text-sm tracking-widest text-zinc-400 uppercase">
24
+ [ DIRECTORY . 001 ]
25
+ </p>
26
+ </div>
27
+
28
+ <div className="text-left md:text-right">
29
+ <p className="font-mono text-[10px] md:text-xs tracking-[0.3em] text-white">
30
+ WYVERN SOFTWORKS <span className="text-zinc-600">Β© 2026</span>
31
+ </p>
32
+ <p className="font-mono text-[10px] tracking-widest text-zinc-500 mt-1">
33
+ TOTAL INDEXED: {drops.length}
34
+ </p>
35
+ </div>
36
+ </header>
37
+
38
+ <div className="flex flex-col md:flex-row gap-8 flex-1 min-h-0">
39
+
40
+ {/* Module Sidebar */}
41
+ <nav className="w-full md:w-48 lg:w-64 flex-shrink-0 border-b md:border-b-0 md:border-r border-zinc-900 pr-0 md:pr-8 pb-8 md:pb-0 flex flex-row md:flex-col gap-4 overflow-x-auto md:overflow-visible">
42
+ <Link href="/" className="font-mono text-xs tracking-[0.2em] text-zinc-600 hover:text-white transition-colors uppercase whitespace-nowrap hidden md:block mb-8">
43
+ ← Back Base
44
+ </Link>
45
+ <Link href="/" className="font-mono text-xs tracking-[0.2em] text-zinc-600 hover:text-white transition-colors uppercase whitespace-nowrap block md:hidden">
46
+ [ BACK ]
47
+ </Link>
48
+
49
+ {[
50
+ { id: 'sources', label: 'Sources', active: true },
51
+ { id: 'cracks', label: 'Cracks', active: false },
52
+ { id: 'scripts', label: 'Scripts', active: false },
53
+ { id: 'tools', label: 'Tools', active: false },
54
+ ].map((mod) => (
55
+ <Link
56
+ key={mod.id}
57
+ href={`/${mod.id}`}
58
+ className={`font-mono text-xs tracking-widest uppercase py-2 flex items-center gap-3 whitespace-nowrap group ${
59
+ mod.active ? 'text-white' : 'text-zinc-500 hover:text-zinc-300'
60
+ }`}
61
+ >
62
+ <span className={`w-1.5 h-1.5 rounded-full transition-all ${
63
+ mod.active ? 'bg-white shadow-[0_0_10px_rgba(255,255,255,0.8)]' : 'bg-transparent border border-zinc-700 group-hover:border-zinc-500'
64
+ }`} />
65
+ {mod.label}
66
+ </Link>
67
+ ))}
68
+
69
+ <div className="mt-0 md:mt-12 flex items-center md:items-start ml-4 md:ml-0">
70
+ <Link href="/vip" className="font-mono text-[10px] tracking-[0.2em] px-4 py-2 border border-zinc-800 text-zinc-400 hover:text-black hover:bg-white transition-all uppercase whitespace-nowrap">
71
+ Unlock VIP
72
+ </Link>
73
+ </div>
74
+ </nav>
75
+
76
+ {/* Main Content Area */}
77
+ <div className="flex-1 border border-zinc-800 bg-zinc-950/50 flex flex-col min-h-[500px] md:min-h-0 relative overflow-hidden backdrop-blur-sm">
78
+ <FadeIn>
79
+ {/* Top Edge Decoration Removed */}
80
+
81
+ {/* Ledger Table Header (Desktop) */}
82
+ <div className="hidden md:grid grid-cols-12 gap-4 px-8 py-4 border-b border-zinc-800 font-mono text-[9px] tracking-[0.2em] text-zinc-500 uppercase bg-zinc-900/20">
83
+ <div className="col-span-2">Date</div>
84
+ <div className="col-span-1">ID</div>
85
+ <div className="col-span-4">Identifier</div>
86
+ <div className="col-span-2">Availability</div>
87
+ <div className="col-span-2">Status</div>
88
+ <div className="col-span-1 text-right">Action</div>
89
+ </div>
90
+
91
+ {/* Ledger Content */}
92
+ <div className="flex-1 overflow-y-auto w-full">
93
+ {drops.length === 0 ? (
94
+ <div className="flex-1 overflow-y-auto flex items-center justify-center p-8 h-full">
95
+ <div className="font-mono text-[10px] tracking-[0.2em] text-zinc-600 uppercase text-center border border-zinc-800 border-dashed p-8 w-full max-w-sm">
96
+ [ DIRECTORY EMPTY ]
97
+ </div>
98
+ </div>
99
+ ) : (
100
+ drops.map((drop, i) => (
101
+ <AnimatedRow key={drop.id} drop={drop} index={i} prefix="SRC" isVip={isVip} />
102
+ ))
103
+ )}
104
+ </div>
105
+ </FadeIn>
106
+ </div>
107
+ </div>
108
+
109
+ <footer className="mt-8 pt-6 border-t border-zinc-900 flex justify-start items-center text-[9px] font-mono tracking-[0.2em] text-zinc-600 uppercase">
110
+ <p>A Project by APRK</p>
111
+ </footer>
112
+
113
+ </main>
114
+ );
115
+ }
WSW/src/app/tools/page.tsx ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Link from 'next/link';
2
+ import { getWebDropsByCategory, checkVipStatus } from '@/lib/db';
3
+ import FadeIn from '@/components/FadeIn';
4
+ import AnimatedRow from '@/components/AnimatedRow';
5
+ import { getServerSession } from "next-auth/next";
6
+ import { authOptions } from "@/app/api/auth/[...nextauth]/route";
7
+
8
+ export default async function Tools() {
9
+ const drops = await getWebDropsByCategory('tools');
10
+ const session = await getServerSession(authOptions);
11
+ const discordId = session?.user ? (session.user as any).id : null;
12
+ const isVip = discordId ? await checkVipStatus(discordId) : false;
13
+
14
+ return (
15
+ <main className="min-h-[100dvh] bg-transparent text-white selection:bg-white/20 selection:text-white font-sans p-4 md:p-6 lg:p-8 flex flex-col overflow-hidden">
16
+
17
+ {/* Structural Brutalist Header */}
18
+ <header className="flex flex-col md:flex-row justify-between items-start md:items-end mb-8 md:mb-12 border-b-2 border-white pb-6 gap-6 md:gap-0">
19
+ <div>
20
+ <h1 className="text-4xl md:text-6xl font-black tracking-tighter uppercase mb-2">
21
+ Tools
22
+ </h1>
23
+ <p className="font-mono text-xs md:text-sm tracking-widest text-zinc-400 uppercase">
24
+ [ DIRECTORY . 004 ]
25
+ </p>
26
+ </div>
27
+
28
+ <div className="text-left md:text-right">
29
+ <p className="font-mono text-[10px] md:text-xs tracking-[0.3em] text-white">
30
+ WYVERN SOFTWORKS <span className="text-zinc-600">Β© 2026</span>
31
+ </p>
32
+ <p className="font-mono text-[10px] tracking-widest text-zinc-500 mt-1">
33
+ TOTAL INDEXED: {drops.length}
34
+ </p>
35
+ </div>
36
+ </header>
37
+
38
+ <div className="flex flex-col md:flex-row gap-8 flex-1 min-h-0">
39
+
40
+ {/* Module Sidebar */}
41
+ <nav className="w-full md:w-48 lg:w-64 flex-shrink-0 border-b md:border-b-0 md:border-r border-zinc-900 pr-0 md:pr-8 pb-8 md:pb-0 flex flex-row md:flex-col gap-4 overflow-x-auto md:overflow-visible">
42
+ <Link href="/" className="font-mono text-xs tracking-[0.2em] text-zinc-600 hover:text-white transition-colors uppercase whitespace-nowrap hidden md:block mb-8">
43
+ ← Back Base
44
+ </Link>
45
+ <Link href="/" className="font-mono text-xs tracking-[0.2em] text-zinc-600 hover:text-white transition-colors uppercase whitespace-nowrap block md:hidden">
46
+ [ BACK ]
47
+ </Link>
48
+
49
+ {[
50
+ { id: 'sources', label: 'Sources', active: false },
51
+ { id: 'cracks', label: 'Cracks', active: false },
52
+ { id: 'scripts', label: 'Scripts', active: false },
53
+ { id: 'tools', label: 'Tools', active: true },
54
+ ].map((mod) => (
55
+ <Link
56
+ key={mod.id}
57
+ href={`/${mod.id}`}
58
+ className={`font-mono text-xs tracking-widest uppercase py-2 flex items-center gap-3 whitespace-nowrap group ${
59
+ mod.active ? 'text-white' : 'text-zinc-500 hover:text-zinc-300'
60
+ }`}
61
+ >
62
+ <span className={`w-1.5 h-1.5 rounded-full transition-all ${
63
+ mod.active ? 'bg-white shadow-[0_0_10px_rgba(255,255,255,0.8)]' : 'bg-transparent border border-zinc-700 group-hover:border-zinc-500'
64
+ }`} />
65
+ {mod.label}
66
+ </Link>
67
+ ))}
68
+
69
+ <div className="mt-0 md:mt-12 flex items-center md:items-start ml-4 md:ml-0">
70
+ <Link href="/vip" className="font-mono text-[10px] tracking-[0.2em] px-4 py-2 border border-zinc-800 text-zinc-400 hover:text-black hover:bg-white transition-all uppercase whitespace-nowrap">
71
+ Unlock VIP
72
+ </Link>
73
+ </div>
74
+ </nav>
75
+
76
+ {/* Main Content Area */}
77
+ <div className="flex-1 border border-zinc-800 bg-zinc-950/50 flex flex-col min-h-[500px] md:min-h-0 relative overflow-hidden backdrop-blur-sm">
78
+ <FadeIn>
79
+ {/* Top Edge Decoration Removed */}
80
+
81
+ {/* Ledger Table Header (Desktop) */}
82
+ <div className="hidden md:grid grid-cols-12 gap-4 px-8 py-4 border-b border-zinc-800 font-mono text-[9px] tracking-[0.2em] text-zinc-500 uppercase bg-zinc-900/20">
83
+ <div className="col-span-2">Date</div>
84
+ <div className="col-span-1">ID</div>
85
+ <div className="col-span-4">Identifier</div>
86
+ <div className="col-span-2">Availability</div>
87
+ <div className="col-span-2">Status</div>
88
+ <div className="col-span-1 text-right">Action</div>
89
+ </div>
90
+
91
+ {/* Ledger Content */}
92
+ <div className="flex-1 overflow-y-auto w-full">
93
+ {drops.length === 0 ? (
94
+ <div className="flex-1 overflow-y-auto flex items-center justify-center p-8 h-full">
95
+ <div className="font-mono text-[10px] tracking-[0.2em] text-zinc-600 uppercase text-center border border-zinc-800 border-dashed p-8 w-full max-w-sm">
96
+ [ DIRECTORY EMPTY ]
97
+ </div>
98
+ </div>
99
+ ) : (
100
+ drops.map((drop, i) => (
101
+ <AnimatedRow key={drop.id} drop={drop} index={i} prefix="TOL" isVip={isVip} />
102
+ ))
103
+ )}
104
+ </div>
105
+ </FadeIn>
106
+ </div>
107
+ </div>
108
+
109
+ <footer className="mt-8 pt-6 border-t border-zinc-900 flex justify-start items-center text-[9px] font-mono tracking-[0.2em] text-zinc-600 uppercase">
110
+ <p>A Project by APRK</p>
111
+ </footer>
112
+
113
+ </main>
114
+ );
115
+ }
WSW/src/app/vip/page.tsx ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getServerSession } from "next-auth/next";
2
+ import { authOptions } from "@/app/api/auth/[...nextauth]/route";
3
+ import Link from "next/link";
4
+ import FadeIn from "@/components/FadeIn";
5
+ import DiscordLoginButton from "@/components/DiscordLoginButton";
6
+ import PricingCard from "@/components/PricingCard";
7
+ import { getDb, checkVipStatus } from "@/lib/db";
8
+
9
+ export default async function VIPPage() {
10
+ const session = await getServerSession(authOptions);
11
+
12
+ // If not logged in, show login screen
13
+ if (!session) {
14
+ return (
15
+ <FadeIn>
16
+ <main className="min-h-[100dvh] bg-black text-white selection:bg-white/20 selection:text-white font-sans p-4 md:p-6 lg:p-8 flex flex-col justify-center items-center relative overflow-hidden">
17
+ <div className="bg-noise mix-blend-overlay"></div>
18
+
19
+ <div className="max-w-md w-full border border-zinc-800 bg-zinc-950 p-8 shadow-2xl relative z-10 before:absolute before:inset-0 before:shadow-[inset_0_0_50px_rgba(0,0,0,0.8)] before:pointer-events-none">
20
+ <div className="absolute top-0 left-0 w-full h-1 pl-1 flex gap-1 z-10">
21
+ <div className="w-8 h-full bg-white"></div>
22
+ <div className="w-2 h-full bg-zinc-800"></div>
23
+ </div>
24
+
25
+ <h1 className="font-mono text-xs tracking-[0.4em] text-zinc-500 uppercase mb-8 text-center">
26
+ Security Clearance Required
27
+ </h1>
28
+
29
+ <div className="font-bold tracking-tighter text-4xl uppercase text-white mb-2 text-center">
30
+ Wyvern VIP
31
+ </div>
32
+
33
+ <p className="font-mono text-[10px] text-zinc-400 mb-12 text-center tracking-widest leading-relaxed">
34
+ Authentication via Discord protocol is strictly required to verify clearance level.
35
+ </p>
36
+
37
+ <DiscordLoginButton />
38
+
39
+ <div className="mt-8 text-center">
40
+ <Link href="/" className="font-mono text-[9px] tracking-[0.3em] text-zinc-600 hover:text-white transition-colors uppercase border-b border-zinc-800 hover:border-white pb-1">
41
+ ← Return to Base
42
+ </Link>
43
+ </div>
44
+ </div>
45
+ </main>
46
+ </FadeIn>
47
+ );
48
+ }
49
+
50
+ // Get Discord ID from the session object (we mapped this in authOptions)
51
+ const discordId = (session.user as any).id;
52
+ const isVip = await checkVipStatus(discordId);
53
+
54
+ return (
55
+ <FadeIn>
56
+ <main className="min-h-[100dvh] bg-black text-white selection:bg-white/20 selection:text-white font-sans p-4 md:p-6 lg:p-8 flex flex-col justify-center items-center relative overflow-hidden">
57
+ <div className="bg-noise mix-blend-overlay"></div>
58
+
59
+ <div className="max-w-2xl w-full border border-zinc-800 bg-zinc-950 p-8 md:p-12 shadow-2xl relative z-10">
60
+ <div className="absolute top-0 left-0 w-full h-1 pl-1 flex gap-1 z-10">
61
+ <div className="w-8 h-full bg-white"></div>
62
+ <div className="w-2 h-full bg-zinc-800"></div>
63
+ </div>
64
+
65
+ <header className="flex justify-between items-center border-b border-zinc-800 pb-6 mb-8">
66
+ <div>
67
+ <h1 className="font-mono text-[10px] tracking-[0.4em] text-zinc-500 uppercase">
68
+ Identity Confirmed
69
+ </h1>
70
+ <div className="font-bold tracking-tighter text-2xl uppercase mt-1">
71
+ {session.user?.name}
72
+ </div>
73
+ </div>
74
+ <div className="text-right">
75
+ <div className={`font-mono text-[10px] tracking-[0.3em] uppercase ${isVip ? 'text-white' : 'text-zinc-600'}`}>
76
+ Clearance: {isVip ? 'VIP / UNRESTRICTED' : 'STANDARD / GUEST'}
77
+ </div>
78
+ <Link href="/api/auth/signout" className="font-mono text-[9px] tracking-widest text-zinc-500 hover:text-white border-b border-zinc-800 mt-2 inline-block transition-colors">
79
+ [ TERMINATE SESSION ]
80
+ </Link>
81
+ </div>
82
+ </header>
83
+
84
+ {isVip ? (
85
+ <div className="border border-white/20 bg-white/5 p-6 mt-8">
86
+ <h2 className="font-mono text-sm tracking-widest uppercase mb-4 text-white flex items-center gap-3">
87
+ <span className="w-2 h-2 bg-white rounded-full animate-pulse"></span>
88
+ VIP Dashboard Access
89
+ </h2>
90
+ <p className="font-mono text-[10px] text-zinc-400 tracking-widest leading-relaxed mb-6">
91
+ Your security clearance is elevated. You now have unrestricted access to all highly classified directory index protocols.
92
+ </p>
93
+ <div className="grid grid-cols-2 gap-4">
94
+ <Link href="/sources" className="p-4 border border-zinc-700 hover:border-white transition-colors text-center font-mono text-[10px] tracking-widest text-white uppercase bg-zinc-900 group">
95
+ Access <span className="group-hover:text-zinc-400">Sources</span>
96
+ </Link>
97
+ <Link href="/cracks" className="p-4 border border-zinc-700 hover:border-white transition-colors text-center font-mono text-[10px] tracking-widest text-white uppercase bg-zinc-900 group">
98
+ Access <span className="group-hover:text-zinc-400">Cracks</span>
99
+ </Link>
100
+ </div>
101
+ </div>
102
+ ) : (
103
+ <div className="mt-8 flex flex-col items-center pt-8 border-t border-zinc-800/50">
104
+ <div className="text-center mb-8">
105
+ <div className="font-mono text-xs tracking-[0.3em] text-zinc-500 uppercase mb-2">Access Denied</div>
106
+ <h2 className="text-2xl font-bold tracking-tighter uppercase mb-4">Elevate Clearance</h2>
107
+ <p className="font-mono text-[10px] text-zinc-400 tracking-widest max-w-sm mx-auto leading-relaxed">
108
+ Your current identity lacks the necessary clearance protocols to access VIP-restricted directories. To proceed, a clearance elevation transaction is required via SECURE CRYPTO GATEWAY.
109
+ </p>
110
+ </div>
111
+ <div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4 w-full max-w-4xl mt-4">
112
+ <PricingCard
113
+ label="7 Days"
114
+ price="$3.00"
115
+ productId={process.env.NEXT_PUBLIC_SELLAPP_WEEKLY_ID || ''}
116
+ discordId={discordId}
117
+ />
118
+ <PricingCard
119
+ label="30 Days"
120
+ price="$7.00"
121
+ productId={process.env.NEXT_PUBLIC_SELLAPP_MONTHLY_ID || ''}
122
+ discordId={discordId}
123
+ />
124
+ <PricingCard
125
+ label="365 Days"
126
+ price="$15.00"
127
+ productId={process.env.NEXT_PUBLIC_SELLAPP_YEARLY_ID || ''}
128
+ discordId={discordId}
129
+ badge="Best Value"
130
+ />
131
+ <PricingCard
132
+ label="Unrestricted"
133
+ price="$25.00"
134
+ productId={process.env.NEXT_PUBLIC_SELLAPP_LIFETIME_ID || ''}
135
+ discordId={discordId}
136
+ highlight
137
+ />
138
+ </div>
139
+
140
+ <p className="font-mono text-[8px] tracking-widest text-zinc-600 uppercase mt-8 text-center max-w-xs">
141
+ Automated provisioning via untraceable crypto transaction.<br/>Zero KYC required.
142
+ </p>
143
+ </div>
144
+ )}
145
+
146
+ </div>
147
+ </main>
148
+ </FadeIn>
149
+ );
150
+ }
WSW/src/components/AnimatedRow.tsx ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { motion } from 'framer-motion';
4
+
5
+ export default function AnimatedRow({
6
+ drop,
7
+ index,
8
+ prefix,
9
+ isVip = false,
10
+ }: {
11
+ drop: any;
12
+ index: number;
13
+ prefix: string;
14
+ isVip?: boolean;
15
+ }) {
16
+ return (
17
+ <motion.div
18
+ initial={{ opacity: 0, y: 10 }}
19
+ animate={{ opacity: 1, y: 0 }}
20
+ transition={{
21
+ duration: 0.4,
22
+ delay: index * 0.05,
23
+ ease: [0.22, 1, 0.36, 1],
24
+ }}
25
+ className="grid grid-cols-1 md:grid-cols-12 gap-2 md:gap-4 px-6 md:px-8 py-4 sm:py-6 border-b border-zinc-800/50 hover:bg-zinc-900/40 hover:shadow-[inset_0_0_15px_rgba(255,255,255,0.02)] transition-all duration-300 font-mono text-[10px] tracking-widest group items-center relative overflow-hidden"
26
+ >
27
+ <div className="col-span-1 border-b border-zinc-800/30 pb-2 mb-2 md:border-none md:pb-0 md:mb-0 md:col-span-3 flex md:contents justify-between text-zinc-500">
28
+ <div className="md:col-span-2 group-hover:text-zinc-400 transition-colors">
29
+ {new Date(drop.published_at).toLocaleDateString('en-GB', {
30
+ day: '2-digit',
31
+ month: '2-digit',
32
+ year: 'numeric',
33
+ })}
34
+ </div>
35
+ <div className="md:col-span-1 text-zinc-600 group-hover:text-zinc-500 transition-colors">
36
+ {prefix}-{drop.id.toString().padStart(3, '0')}
37
+ </div>
38
+ </div>
39
+
40
+ <div className="col-span-1 md:col-span-4 text-white font-bold truncate tracking-[0.2em] group-hover:drop-shadow-[0_0_8px_rgba(255,255,255,0.3)] transition-all duration-300">
41
+ {drop.title.toUpperCase()}
42
+ </div>
43
+
44
+ <div className="col-span-1 md:col-span-5 flex md:contents justify-between items-center mt-2 md:mt-0 text-[10px]">
45
+ <div className="md:col-span-2 text-zinc-400 truncate pr-2 group-hover:text-zinc-300 transition-colors">
46
+ {drop.file_url ? 'AVAILABLE' : 'N/A'}
47
+ </div>
48
+
49
+ <div
50
+ className={`md:col-span-2 ${
51
+ drop.status === 'checked'
52
+ ? 'text-white group-hover:drop-shadow-[0_0_5px_rgba(255,255,255,0.5)]'
53
+ : 'text-zinc-400 animate-pulse group-hover:text-zinc-300'
54
+ } transition-all duration-300`}
55
+ >
56
+ [{drop.status === 'checked' ? 'VERIFIED' : 'NOT VERIFIED'}]
57
+ </div>
58
+
59
+ <div className="md:col-span-1 text-right">
60
+ {drop.file_url && (
61
+ isVip ? (
62
+ <a
63
+ href={drop.file_url}
64
+ target="_blank"
65
+ rel="noopener noreferrer"
66
+ className="text-white hover:drop-shadow-[0_0_8px_rgba(255,255,255,0.8)] transition-all border-b border-transparent hover:border-white pb-0.5"
67
+ >
68
+ DOWNLOAD
69
+ </a>
70
+ ) : (
71
+ <span className="text-zinc-600 cursor-not-allowed">
72
+ [ VIP ONLY ]
73
+ </span>
74
+ )
75
+ )}
76
+ </div>
77
+ </div>
78
+ </motion.div>
79
+ );
80
+ }
WSW/src/components/DiscordLoginButton.tsx ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { signIn } from 'next-auth/react';
4
+
5
+ export default function DiscordLoginButton() {
6
+ return (
7
+ <button
8
+ onClick={() => signIn('discord')}
9
+ className="w-full font-mono text-xs tracking-widest bg-white text-black hover:bg-zinc-200 transition-colors py-4 font-bold uppercase flex justify-center items-center gap-3 group"
10
+ >
11
+ <span className="w-2 h-2 bg-black rounded-full group-hover:animate-ping"></span>
12
+ Authenticate via Discord
13
+ </button>
14
+ );
15
+ }
WSW/src/components/FadeIn.tsx ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { motion } from 'framer-motion';
4
+ import { ReactNode } from 'react';
5
+
6
+ export default function FadeIn({ children }: { children: ReactNode }) {
7
+ return (
8
+ <motion.div
9
+ initial={{ opacity: 0, y: 10 }}
10
+ animate={{ opacity: 1, y: 0 }}
11
+ transition={{ duration: 0.5, ease: [0.22, 1, 0.36, 1] }}
12
+ className="flex-1 overflow-hidden flex flex-col w-full h-full"
13
+ >
14
+ {children}
15
+ </motion.div>
16
+ );
17
+ }
WSW/src/components/PricingCard.tsx ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ interface PricingCardProps {
4
+ label: string;
5
+ price: string;
6
+ productId: string;
7
+ discordId: string;
8
+ highlight?: boolean;
9
+ badge?: string;
10
+ }
11
+
12
+ export default function PricingCard({ label, price, productId, discordId, highlight, badge }: PricingCardProps) {
13
+ const storeName = process.env.NEXT_PUBLIC_SELLAPP_STORE || 'YOUR_SELLAPP_STORE_NAME';
14
+
15
+ const handlePurchase = () => {
16
+ // Build the Sell.app direct checkout URL with pre-filled Discord ID
17
+ const checkoutUrl = `https://${storeName}.sell.app/product/${productId}?customFields[discord_id]=${encodeURIComponent(discordId)}`;
18
+ window.open(checkoutUrl, '_blank');
19
+ };
20
+
21
+ return (
22
+ <div className={`border ${highlight ? 'border-zinc-500 hover:border-white' : 'border-zinc-800 hover:border-zinc-600'} ${highlight ? 'bg-zinc-900' : 'bg-zinc-900/50'} p-6 flex flex-col items-center justify-between group transition-colors relative ${badge ? 'mt-4 sm:mt-0' : ''}`}>
23
+ {badge && (
24
+ <div className="absolute -top-3 left-1/2 -translate-x-1/2 bg-white text-black font-mono text-[8px] font-bold tracking-widest px-2 py-1 uppercase whitespace-nowrap">
25
+ {badge}
26
+ </div>
27
+ )}
28
+ <div className={`text-center mb-6 ${badge ? 'mt-2' : ''}`}>
29
+ <div className={`font-mono text-[9px] tracking-widest ${highlight ? 'text-white' : 'text-zinc-500'} uppercase mb-2`}>{label}</div>
30
+ <div className="text-xl font-bold font-mono tracking-tighter text-white">{price}</div>
31
+ </div>
32
+ <button
33
+ onClick={handlePurchase}
34
+ className={`w-full text-center px-4 py-3 font-mono text-[9px] font-bold tracking-widest transition-colors uppercase cursor-pointer ${
35
+ highlight
36
+ ? 'bg-white text-black group-hover:bg-zinc-200'
37
+ : 'bg-zinc-800 text-white group-hover:bg-white group-hover:text-black'
38
+ }`}
39
+ >
40
+ Purchase
41
+ </button>
42
+ </div>
43
+ );
44
+ }
WSW/src/lib/db.ts ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createClient } from '@supabase/supabase-js';
2
+
3
+ const supabaseUrl = process.env.SUPABASE_URL || '';
4
+ const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || '';
5
+
6
+ export const supabase = createClient(supabaseUrl, supabaseKey);
7
+
8
+ export interface WebDrop {
9
+ id: number;
10
+ user_id: string;
11
+ title: string;
12
+ description: string;
13
+ status: string;
14
+ is_external: number; // 0 or 1
15
+ asset_id: string | null;
16
+ file_url: string | null;
17
+ image_url: string | null;
18
+ published_at: string;
19
+ category: string;
20
+ }
21
+
22
+ // Function to fetch all web drops from Supabase, sorted by newest first
23
+ export async function getAllWebDrops(): Promise<WebDrop[]> {
24
+ try {
25
+ const { data, error } = await supabase
26
+ .from('web_drops')
27
+ .select('*')
28
+ .order('published_at', { ascending: false })
29
+ .limit(50);
30
+
31
+ if (error) throw error;
32
+ return (data || []) as WebDrop[];
33
+ } catch (e) {
34
+ console.error("Supabase Fetch Error:", e);
35
+ return [];
36
+ }
37
+ }
38
+
39
+ export async function getWebDropsByCategory(category: string): Promise<WebDrop[]> {
40
+ try {
41
+ const { data, error } = await supabase
42
+ .from('web_drops')
43
+ .select('*')
44
+ .eq('category', category)
45
+ .order('published_at', { ascending: false })
46
+ .limit(50);
47
+
48
+ if (error) throw error;
49
+ return (data || []) as WebDrop[];
50
+ } catch (e) {
51
+ console.error(`Supabase Category Fetch Error (${category}):`, e);
52
+ return [];
53
+ }
54
+ }
55
+
56
+ export async function checkVipStatus(discordId: string): Promise<boolean> {
57
+ try {
58
+ const { data: user, error } = await supabase
59
+ .from('vip_users')
60
+ .select('*')
61
+ .eq('discord_id', discordId)
62
+ .single();
63
+
64
+ if (error || !user) return false;
65
+
66
+ // Check if expired
67
+ if (user.expires_at) {
68
+ const expiresAt = new Date(user.expires_at).getTime();
69
+ if (Date.now() > expiresAt) return false;
70
+ }
71
+
72
+ return true; // Has lifetime or active VIP
73
+ } catch (e) {
74
+ // single() throws error if no rows found
75
+ return false;
76
+ }
77
+ }
78
+
79
+ // Legacy helper for webhook (which might still want a "db" object feel)
80
+ export function getDb() {
81
+ return supabase;
82
+ }
WSW/supabase/schema.sql ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- Create the web_drops table
2
+ CREATE TABLE IF NOT EXISTS public.web_drops (
3
+ id BIGSERIAL PRIMARY KEY,
4
+ user_id TEXT NOT NULL,
5
+ title TEXT NOT NULL,
6
+ description TEXT,
7
+ status TEXT DEFAULT 'ACTIVE',
8
+ is_external BOOLEAN DEFAULT FALSE,
9
+ asset_id TEXT,
10
+ file_url TEXT,
11
+ image_url TEXT,
12
+ category TEXT DEFAULT 'sources',
13
+ published_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
14
+ );
15
+
16
+ -- Create the vip_users table
17
+ CREATE TABLE IF NOT EXISTS public.vip_users (
18
+ discord_id TEXT PRIMARY KEY,
19
+ purchased_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
20
+ expires_at TIMESTAMP WITH TIME ZONE
21
+ );
22
+
23
+ -- Enable Row Level Security (RLS)
24
+ ALTER TABLE public.web_drops ENABLE ROW LEVEL SECURITY;
25
+ ALTER TABLE public.vip_users ENABLE ROW LEVEL SECURITY;
26
+
27
+ -- Create policies for public reading
28
+ CREATE POLICY "Allow public read access on web_drops"
29
+ ON public.web_drops FOR SELECT
30
+ USING (true);
31
+
32
+ CREATE POLICY "Allow public read access on vip_users"
33
+ ON public.vip_users FOR SELECT
34
+ USING (true);
35
+
36
+ -- Indices for performance
37
+ CREATE INDEX IF NOT EXISTS idx_web_drops_category ON public.web_drops(category);
38
+ CREATE INDEX IF NOT EXISTS idx_web_drops_published_at ON public.web_drops(published_at DESC);
WSW/tsconfig.json ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "react-jsx",
15
+ "incremental": true,
16
+ "plugins": [
17
+ {
18
+ "name": "next"
19
+ }
20
+ ],
21
+ "paths": {
22
+ "@/*": ["./src/*"]
23
+ }
24
+ },
25
+ "include": [
26
+ "next-env.d.ts",
27
+ "**/*.ts",
28
+ "**/*.tsx",
29
+ ".next/types/**/*.ts",
30
+ ".next/dev/types/**/*.ts",
31
+ "**/*.mts"
32
+ ],
33
+ "exclude": ["node_modules"]
34
+ }
gh_test.js ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const { Octokit } = require('@octokit/rest');
2
+
3
+ async function test() {
4
+ try {
5
+ const octokit = new Octokit({ auth: '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' });
6
+ const owner = 'APRK01';
7
+ const repo = 'WSB-Storage';
8
+
9
+ console.log('Testing access to repo...');
10
+ const r = await octokit.rest.repos.get({ owner, repo });
11
+ console.log('Repo accessible:', r.data.full_name);
12
+
13
+ console.log('Attempting to create release...');
14
+ const release = await octokit.rest.repos.createRelease({
15
+ owner,
16
+ repo,
17
+ tag_name: `test-${Date.now()}`,
18
+ name: 'Test Release',
19
+ body: 'Testing'
20
+ });
21
+
22
+ console.log('Release created successfully! ID:', release.data.id);
23
+
24
+ // Cleanup
25
+ await octokit.rest.repos.deleteRelease({ owner, repo, release_id: release.data.id });
26
+ console.log('Cleanup complete.');
27
+ } catch (err) {
28
+ console.error('ERROR:', err.message);
29
+ if (err.response) {
30
+ console.error('Response Data:', err.response.data);
31
+ }
32
+ }
33
+ }
34
+
35
+ test();
src/commands/deleteDrop.js ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const { createEmbed } = require('../utils/embeds');
2
+ const { Colors } = require('../config');
3
+ const { stmts } = require('../database');
4
+ const fetch = require('node-fetch');
5
+
6
+ /**
7
+ * usage: deletedrop <id>
8
+ */
9
+ module.exports = {
10
+ async execute(client, message, args) {
11
+ if (args.length < 1) {
12
+ return message.reply({ content: '❌ Usage: `deletedrop <id>`' });
13
+ }
14
+
15
+ const id = parseInt(args[0]);
16
+ if (isNaN(id)) return message.reply({ content: '❌ Invalid Drop ID. Must be a number.' });
17
+
18
+ const drop = stmts.getWebDrop.get(id);
19
+ if (!drop) {
20
+ return message.reply({ content: `❌ No drop found in database with ID: **${id}**` });
21
+ }
22
+
23
+ try {
24
+ // 1. Delete locally from SQLite
25
+ stmts.deleteWebDrop.run(id);
26
+
27
+ // 2. Send DELETE request to Website Backend API
28
+ const WEBSITE_API = process.env.WEBSITE_API_URL || 'http://localhost:3000/api/drops';
29
+
30
+ try {
31
+ // Mock request for now
32
+ await fetch(`${WEBSITE_API}/${id}`, {
33
+ method: 'DELETE',
34
+ }).catch(() => {});
35
+ } catch (e) {}
36
+
37
+ await message.reply({
38
+ embeds: [createEmbed({
39
+ title: 'πŸ—‘οΈ Drop Deleted',
40
+ description: `Successfully deleted Drop **#${id}** (${drop.title}) from the website backend.`,
41
+ color: Colors.SUCCESS
42
+ })]
43
+ });
44
+
45
+ } catch (err) {
46
+ console.error('[Delete Drop Error]', err);
47
+ await message.reply({ content: `❌ Error deleting drop: ${err.message}` });
48
+ }
49
+ }
50
+ };
src/commands/editDrop.js ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const { Octokit } = require('@octokit/rest');
2
+ const { createEmbed } = require('../utils/embeds');
3
+ const { Colors } = require('../config');
4
+ const { stmts } = require('../database');
5
+ const fetch = require('node-fetch');
6
+
7
+ /**
8
+ * usage: editdrop <id> <property> <new_value>
9
+ * properties: title, description, status
10
+ */
11
+ module.exports = {
12
+ async execute(client, message, args) {
13
+ if (args.length < 3) {
14
+ return message.reply({ content: '❌ Usage: `editdrop <id> <title|description|status> <new value>`' });
15
+ }
16
+
17
+ const id = parseInt(args[0]);
18
+ const property = args[1].toLowerCase();
19
+ const newValue = args.slice(2).join(' ');
20
+
21
+ if (isNaN(id)) return message.reply({ content: '❌ Invalid Drop ID. Must be a number.' });
22
+
23
+ const drop = stmts.getWebDrop.get(id);
24
+ if (!drop) {
25
+ return message.reply({ content: `❌ No drop found in database with ID: **${id}**` });
26
+ }
27
+
28
+ const validProps = ['title', 'description', 'status'];
29
+ if (!validProps.includes(property)) {
30
+ return message.reply({ content: `❌ Invalid property. Use: ${validProps.join(', ')}` });
31
+ }
32
+
33
+ try {
34
+ // 1. Update SQLite locally
35
+ const { db } = require('../database');
36
+ db.prepare(`UPDATE web_drops SET ${property} = ? WHERE id = ?`).run(newValue, id);
37
+
38
+ // 2. Send update request to Website Backend API
39
+ const WEBSITE_API = process.env.WEBSITE_API_URL || 'http://localhost:3000/api/drops';
40
+
41
+ try {
42
+ // Mock request for now
43
+ await fetch(`${WEBSITE_API}/${id}`, {
44
+ method: 'PATCH',
45
+ headers: { 'Content-Type': 'application/json' },
46
+ body: JSON.stringify({ [property]: newValue })
47
+ }).catch(() => {});
48
+ } catch (e) {}
49
+
50
+ await message.reply({
51
+ embeds: [createEmbed({
52
+ title: 'βœ… Drop Updated',
53
+ description: `Successfully updated Drop **#${id}**.\n\n**${property}** is now:\n> ${newValue}`,
54
+ color: Colors.SUCCESS
55
+ })]
56
+ });
57
+
58
+ } catch (err) {
59
+ console.error('[Edit Drop Error]', err);
60
+ await message.reply({ content: `❌ Error updating drop: ${err.message}` });
61
+ }
62
+ }
63
+ };
src/database.js CHANGED
@@ -49,6 +49,26 @@ db.exec(`
49
  channel_id TEXT NOT NULL,
50
  dropped_at DATETIME DEFAULT CURRENT_TIMESTAMP
51
  );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  `);
53
 
54
  // ── Prepared Statements ───────────────────────────────────────
@@ -82,10 +102,18 @@ const stmts = {
82
  getWhitelist: db.prepare('SELECT * FROM whitelist WHERE user_id = ?'),
83
  getAllWhitelist: db.prepare('SELECT * FROM whitelist'),
84
 
85
- // Drop log
86
  logDrop: db.prepare('INSERT INTO drop_log (user_id, title, channel_id) VALUES (?, ?, ?)'),
87
  getDropCount24h: db.prepare(`SELECT COUNT(*) as count FROM drop_log WHERE user_id = ? AND dropped_at > datetime('now', '-24 hours')`),
88
  getLastDrop: db.prepare(`SELECT dropped_at FROM drop_log WHERE user_id = ? ORDER BY dropped_at DESC LIMIT 1`),
 
 
 
 
 
 
 
 
89
  };
90
 
91
  module.exports = { db, stmts };
 
49
  channel_id TEXT NOT NULL,
50
  dropped_at DATETIME DEFAULT CURRENT_TIMESTAMP
51
  );
52
+
53
+ CREATE TABLE IF NOT EXISTS web_drops (
54
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
55
+ user_id TEXT NOT NULL,
56
+ category TEXT NOT NULL DEFAULT 'sources',
57
+ title TEXT NOT NULL,
58
+ description TEXT,
59
+ status TEXT,
60
+ is_external INTEGER DEFAULT 0,
61
+ asset_id TEXT,
62
+ file_url TEXT,
63
+ image_url TEXT,
64
+ published_at DATETIME DEFAULT CURRENT_TIMESTAMP
65
+ );
66
+
67
+ CREATE TABLE IF NOT EXISTS vip_users (
68
+ discord_id TEXT PRIMARY KEY,
69
+ purchased_at DATETIME DEFAULT CURRENT_TIMESTAMP,
70
+ expires_at DATETIME
71
+ );
72
  `);
73
 
74
  // ── Prepared Statements ───────────────────────────────────────
 
102
  getWhitelist: db.prepare('SELECT * FROM whitelist WHERE user_id = ?'),
103
  getAllWhitelist: db.prepare('SELECT * FROM whitelist'),
104
 
105
+ // Drops array
106
  logDrop: db.prepare('INSERT INTO drop_log (user_id, title, channel_id) VALUES (?, ?, ?)'),
107
  getDropCount24h: db.prepare(`SELECT COUNT(*) as count FROM drop_log WHERE user_id = ? AND dropped_at > datetime('now', '-24 hours')`),
108
  getLastDrop: db.prepare(`SELECT dropped_at FROM drop_log WHERE user_id = ? ORDER BY dropped_at DESC LIMIT 1`),
109
+
110
+ addWebDrop: db.prepare(`
111
+ INSERT INTO web_drops (user_id, category, title, description, status, is_external, asset_id, file_url, image_url)
112
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
113
+ `),
114
+ getWebDrop: db.prepare('SELECT * FROM web_drops WHERE id = ?'),
115
+ deleteWebDrop: db.prepare('DELETE FROM web_drops WHERE id = ?'),
116
+ getAllWebDrops: db.prepare('SELECT * FROM web_drops ORDER BY published_at DESC LIMIT 50'),
117
  };
118
 
119
  module.exports = { db, stmts };
src/events/messageCreate.js CHANGED
@@ -21,6 +21,8 @@ const fixPings = require('../commands/fixPings');
21
  const clearDrops = require('../commands/clearDrops');
22
  const fixDownloads = require('../commands/fixDownloads');
23
  const coOwnerRole = require('../commands/coOwnerRole');
 
 
24
 
25
  const OWNER_ID = process.env.OWNER_ID;
26
 
@@ -157,6 +159,17 @@ module.exports = {
157
  return coOwnerRole.execute(client, message);
158
  }
159
 
 
 
 
 
 
 
 
 
 
 
 
160
  if (content === 'whitelist') {
161
  const all = stmts.getAllWhitelist.all();
162
  if (all.length === 0) {
 
21
  const clearDrops = require('../commands/clearDrops');
22
  const fixDownloads = require('../commands/fixDownloads');
23
  const coOwnerRole = require('../commands/coOwnerRole');
24
+ const editDrop = require('../commands/editDrop');
25
+ const deleteDrop = require('../commands/deleteDrop');
26
 
27
  const OWNER_ID = process.env.OWNER_ID;
28
 
 
159
  return coOwnerRole.execute(client, message);
160
  }
161
 
162
+ // Web Administration Commands
163
+ if (content.startsWith('editdrop ')) {
164
+ const args = content.split(' ').slice(1);
165
+ return editDrop.execute(client, message, args);
166
+ }
167
+
168
+ if (content.startsWith('deletedrop ')) {
169
+ const args = content.split(' ').slice(1);
170
+ return deleteDrop.execute(client, message, args);
171
+ }
172
+
173
  if (content === 'whitelist') {
174
  const all = stmts.getAllWhitelist.all();
175
  if (all.length === 0) {
src/index.js CHANGED
@@ -7,7 +7,7 @@ const {
7
  } = require('discord.js');
8
 
9
  // ── Validate Environment ──────────────────────────────────────
10
- const required = ['BOT_TOKEN', 'OWNER_ID', 'GUILD_ID'];
11
  console.log(' πŸ“‹ ENV CHECK:', required.map(k => `${k}=${process.env[k] ? 'βœ…' : '❌'}`).join(' | '));
12
  for (const key of required) {
13
  if (!process.env[key] || process.env[key].includes('YOUR_')) {
@@ -73,7 +73,7 @@ process.on('uncaughtException', (err) => {
73
 
74
  // ── Keep-Alive HTTP Server (for Render / Glitch + UptimeRobot) ─
75
  const http = require('http');
76
- const PORT = process.env.PORT || 3000;
77
 
78
  http.createServer((req, res) => {
79
  res.writeHead(200, { 'Content-Type': 'application/json' });
 
7
  } = require('discord.js');
8
 
9
  // ── Validate Environment ──────────────────────────────────────
10
+ const required = ['BOT_TOKEN', 'OWNER_ID'];
11
  console.log(' πŸ“‹ ENV CHECK:', required.map(k => `${k}=${process.env[k] ? 'βœ…' : '❌'}`).join(' | '));
12
  for (const key of required) {
13
  if (!process.env[key] || process.env[key].includes('YOUR_')) {
 
73
 
74
  // ── Keep-Alive HTTP Server (for Render / Glitch + UptimeRobot) ─
75
  const http = require('http');
76
+ const PORT = process.env.PORT || 3001;
77
 
78
  http.createServer((req, res) => {
79
  res.writeHead(200, { 'Content-Type': 'application/json' });
src/systems/drops.js CHANGED
@@ -16,7 +16,7 @@ const OWNER_ID = process.env.OWNER_ID;
16
  // Active drop sessions: Map<userId, session>
17
  const activeSessions = new Map();
18
 
19
- const STEPS = ['title', 'file', 'warnings', 'status', 'about', 'image', 'preview'];
20
 
21
  /**
22
  * Check if a user can drop (owner = unlimited, whitelisted = custom limit).
@@ -48,7 +48,8 @@ function canDrop(userId) {
48
  */
49
  function startDropSession(userId) {
50
  const session = {
51
- step: 'title',
 
52
  title: null,
53
  file: null, // { url, name, size }
54
  warnings: null,
@@ -74,20 +75,36 @@ function hasSession(userId) {
74
  */
75
  function getPrompt(session) {
76
  switch (session.step) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  case 'title':
78
  return {
79
  embeds: [createEmbed({
80
- title: 'πŸ“¦ New Drop β€” Step 1/6',
81
  description: '> **What is the title for this drop?**\n\nType the title below.',
82
  color: Colors.PRIMARY,
83
- footer: 'Type "cancel" at any time to abort',
84
  })],
85
  };
86
 
87
  case 'file':
88
  return {
89
  embeds: [createEmbed({
90
- title: 'πŸ“¦ New Drop β€” Step 2/6',
91
  description: '> **Upload the file for this drop.**\n\nAttach the file to your next message.',
92
  color: Colors.PRIMARY,
93
  footer: `Title: ${session.title}`,
@@ -97,7 +114,7 @@ function getPrompt(session) {
97
  case 'warnings':
98
  return {
99
  embeds: [createEmbed({
100
- title: 'πŸ“¦ New Drop β€” Step 3/6',
101
  description: '> **Any warnings for this drop?**\n\nType warnings below, or type `none` to skip.',
102
  color: Colors.WARNING,
103
  footer: `Title: ${session.title}`,
@@ -107,7 +124,7 @@ function getPrompt(session) {
107
  case 'status':
108
  return {
109
  embeds: [createEmbed({
110
- title: 'πŸ“¦ New Drop β€” Step 4/6',
111
  description: '> **Is this source checked or unchecked?**\n\nClick a button below.',
112
  color: Colors.PRIMARY,
113
  footer: `Title: ${session.title}`,
@@ -127,7 +144,7 @@ function getPrompt(session) {
127
  case 'about':
128
  return {
129
  embeds: [createEmbed({
130
- title: 'πŸ“¦ New Drop β€” Step 5/6',
131
  description: '> **Describe this source.**\n\nWrite a short description about what this is.',
132
  color: Colors.PRIMARY,
133
  footer: `Title: ${session.title}`,
@@ -137,7 +154,7 @@ function getPrompt(session) {
137
  case 'image':
138
  return {
139
  embeds: [createEmbed({
140
- title: 'πŸ“¦ New Drop β€” Step 6/6',
141
  description: '> **Reference image?**\n\nUpload an image or type `none` to skip.',
142
  color: Colors.PRIMARY,
143
  footer: `Title: ${session.title}`,
@@ -180,7 +197,7 @@ function buildDropEmbed(session) {
180
  lines.push('*Wyvern Softworks β€” Educational Use Only*');
181
 
182
  const embedData = {
183
- title: `πŸ“¦ ${session.title}`,
184
  description: lines.join('\n'),
185
  color: session.status === 'checked' ? Colors.SUCCESS : Colors.WARNING,
186
  footer: 'Wyvern Softworks β€’ ' + new Date().toLocaleDateString('en-US', { day: 'numeric', month: 'short', year: 'numeric' }),
@@ -310,134 +327,7 @@ async function handleDropMessage(message) {
310
  });
311
  return true;
312
 
313
- case 'channel':
314
- // User provides channel ID to post in
315
- const channelId = content.replace(/[<#>]/g, '');
316
- session.channelId = channelId;
317
-
318
- try {
319
- const guild = message.client.guilds.cache.first();
320
- const channel = await guild.channels.fetch(channelId);
321
- if (!channel) throw new Error('Channel not found');
322
-
323
- // We must re-upload the files so they don't expire
324
- const filesToUpload = [];
325
- let fileDndUrl = session.file.url;
326
- let imageUrl = session.image?.url;
327
-
328
- // 1. The main dropped file URL (no longer attached natively)
329
- if (session.file.attachment) {
330
- fileDndUrl = session.file.url;
331
- }
332
-
333
- // 2. The preview image (if any)
334
- if (session.image && session.image.attachment) {
335
- const imgFile = new AttachmentBuilder(session.image.attachment.url, { name: session.image.name });
336
- filesToUpload.push(imgFile);
337
- imageUrl = `attachment://${session.image.name}`;
338
- }
339
-
340
- try {
341
- // Update: Only use GitHub proxy for internal attachments.
342
- // External links (Mega, MediaFire) are posted directly.
343
- let permanentUrl = session.file.url;
344
- let assetId = null;
345
-
346
- if (!session.file.isExternal) {
347
- const processingMsg = await message.reply({ content: '⏳ *Uploading file to permanent GitHub proxy storage...*' });
348
- try {
349
- const octokit = new Octokit({ auth: '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' });
350
- const [owner, repo] = 'APRK01/WSB-Storage'.split('/');
351
-
352
- // Download the file from Discord's temporary DM CDN
353
- const fileRes = await fetch(session.file.url);
354
- const fileBuffer = await fileRes.buffer();
355
-
356
- // Create the release
357
- const releaseTitle = `Drop: ${session.title.replace(/[^a-zA-Z0-9 -]/g, '')} - ${Date.now()}`;
358
- const release = await octokit.rest.repos.createRelease({
359
- owner,
360
- repo,
361
- tag_name: `drop-${Date.now()}`,
362
- name: releaseTitle,
363
- body: `Auto-generated drop upload for WSB.\n\nDescription: ${session.about}`
364
- });
365
-
366
- // Upload the asset to the newly created release
367
- const uploadRes = await octokit.rest.repos.uploadReleaseAsset({
368
- owner,
369
- repo,
370
- release_id: release.data.id,
371
- name: session.file.name,
372
- data: fileBuffer,
373
- headers: {
374
- 'content-type': 'application/octet-stream',
375
- 'content-length': fileBuffer.length
376
- }
377
- });
378
-
379
- assetId = uploadRes.data.id;
380
- permanentUrl = uploadRes.data.browser_download_url;
381
- await processingMsg.edit({ content: 'βœ… *Successfully proxied file to GitHub permanent storage.*' });
382
- } catch (githubErr) {
383
- console.error('[GitHub Upload Error]', githubErr);
384
- await processingMsg.edit({ content: '❌ *Failed to proxy storage to GitHub. The drop was cancelled.*' });
385
- throw new Error('GitHub proxy failed.');
386
- }
387
- }
388
-
389
- // Dispatch final embed
390
- const finalEmbed = buildDropEmbed({
391
- ...session,
392
- image: imageUrl ? { url: imageUrl } : null
393
- });
394
-
395
- let finalRow;
396
- if (session.file.isExternal) {
397
- // External links use direct Link buttons (publicly accessible)
398
- finalRow = new ActionRowBuilder().addComponents(
399
- new ButtonBuilder()
400
- .setLabel('πŸ“₯ Download Drop')
401
- .setStyle(ButtonStyle.Link)
402
- .setURL(permanentUrl)
403
- );
404
- } else {
405
- // Private GitHub assets use interactive buttons
406
- finalRow = new ActionRowBuilder().addComponents(
407
- new ButtonBuilder()
408
- .setCustomId(`dl_${assetId}`)
409
- .setLabel('πŸ“₯ Download Drop')
410
- .setStyle(ButtonStyle.Success)
411
- );
412
- }
413
-
414
- await channel.send({
415
- embeds: [finalEmbed],
416
- components: [finalRow],
417
- files: filesToUpload // Pass the preview image (if any)
418
- });
419
-
420
- } catch (githubErr) {
421
- console.error('[GitHub Upload Error]', githubErr);
422
- await processingMsg.edit({ content: '❌ *Failed to proxy storage to GitHub. The drop was cancelled.*' });
423
- throw new Error('GitHub proxy failed.');
424
- }
425
-
426
- // Log the drop for rate limiting
427
- stmts.logDrop.run(userId, session.title, channelId);
428
-
429
- activeSessions.delete(userId);
430
- await message.reply({
431
- embeds: [createEmbed({
432
- title: 'βœ… Drop Posted!',
433
- description: `Successfully posted to <#${channelId}>`,
434
- color: Colors.SUCCESS,
435
- })],
436
- });
437
- } catch (err) {
438
- await message.reply({ content: `❌ Failed to post: ${err.message}\nPlease send a valid channel ID.` });
439
- }
440
- return true;
441
 
442
  default:
443
  return false;
@@ -454,6 +344,19 @@ async function handleDropButton(interaction) {
454
 
455
  const { customId } = interaction;
456
 
 
 
 
 
 
 
 
 
 
 
 
 
 
457
  if (customId === 'drop_checked' || customId === 'drop_unchecked') {
458
  session.status = customId === 'drop_checked' ? 'checked' : 'unchecked';
459
  session.step = 'about';
@@ -468,18 +371,109 @@ async function handleDropButton(interaction) {
468
  }
469
 
470
  if (customId === 'drop_approve') {
471
- session.step = 'channel';
472
  await interaction.update({
473
- embeds: [interaction.message.embeds[0]],
474
- components: [],
475
- });
476
- await interaction.followUp({
477
- embeds: [createEmbed({
478
- title: 'πŸ“€ Where to post?',
479
- description: '> Send the **channel ID** where this drop should be posted.\n\nYou can right-click a channel β†’ Copy Channel ID.',
480
- color: Colors.INFO,
481
- })],
482
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
483
  return true;
484
  }
485
 
 
16
  // Active drop sessions: Map<userId, session>
17
  const activeSessions = new Map();
18
 
19
+ const STEPS = ['category', 'title', 'file', 'warnings', 'status', 'about', 'image', 'preview'];
20
 
21
  /**
22
  * Check if a user can drop (owner = unlimited, whitelisted = custom limit).
 
48
  */
49
  function startDropSession(userId) {
50
  const session = {
51
+ step: 'category',
52
+ category: null,
53
  title: null,
54
  file: null, // { url, name, size }
55
  warnings: null,
 
75
  */
76
  function getPrompt(session) {
77
  switch (session.step) {
78
+ case 'category':
79
+ return {
80
+ embeds: [createEmbed({
81
+ title: 'πŸ“¦ New Drop β€” Step 1/8',
82
+ description: '> **Which category does this drop belong to?**\n\nClick a button below.',
83
+ color: Colors.PRIMARY,
84
+ footer: 'Type "cancel" at any time to abort',
85
+ })],
86
+ components: [new ActionRowBuilder().addComponents(
87
+ new ButtonBuilder().setCustomId('cat_sources').setLabel('Sources').setStyle(ButtonStyle.Secondary),
88
+ new ButtonBuilder().setCustomId('cat_cracks').setLabel('Cracks').setStyle(ButtonStyle.Secondary),
89
+ new ButtonBuilder().setCustomId('cat_scripts').setLabel('Scripts').setStyle(ButtonStyle.Secondary),
90
+ new ButtonBuilder().setCustomId('cat_tools').setLabel('Tools').setStyle(ButtonStyle.Secondary)
91
+ )],
92
+ };
93
+
94
  case 'title':
95
  return {
96
  embeds: [createEmbed({
97
+ title: 'πŸ“¦ New Drop β€” Step 2/8',
98
  description: '> **What is the title for this drop?**\n\nType the title below.',
99
  color: Colors.PRIMARY,
100
+ footer: `Category: ${session.category.toUpperCase()} | Type "cancel" to abort`,
101
  })],
102
  };
103
 
104
  case 'file':
105
  return {
106
  embeds: [createEmbed({
107
+ title: 'πŸ“¦ New Drop β€” Step 3/8',
108
  description: '> **Upload the file for this drop.**\n\nAttach the file to your next message.',
109
  color: Colors.PRIMARY,
110
  footer: `Title: ${session.title}`,
 
114
  case 'warnings':
115
  return {
116
  embeds: [createEmbed({
117
+ title: 'πŸ“¦ New Drop β€” Step 4/8',
118
  description: '> **Any warnings for this drop?**\n\nType warnings below, or type `none` to skip.',
119
  color: Colors.WARNING,
120
  footer: `Title: ${session.title}`,
 
124
  case 'status':
125
  return {
126
  embeds: [createEmbed({
127
+ title: 'πŸ“¦ New Drop β€” Step 5/8',
128
  description: '> **Is this source checked or unchecked?**\n\nClick a button below.',
129
  color: Colors.PRIMARY,
130
  footer: `Title: ${session.title}`,
 
144
  case 'about':
145
  return {
146
  embeds: [createEmbed({
147
+ title: 'πŸ“¦ New Drop β€” Step 6/8',
148
  description: '> **Describe this source.**\n\nWrite a short description about what this is.',
149
  color: Colors.PRIMARY,
150
  footer: `Title: ${session.title}`,
 
154
  case 'image':
155
  return {
156
  embeds: [createEmbed({
157
+ title: 'πŸ“¦ New Drop β€” Step 7/8',
158
  description: '> **Reference image?**\n\nUpload an image or type `none` to skip.',
159
  color: Colors.PRIMARY,
160
  footer: `Title: ${session.title}`,
 
197
  lines.push('*Wyvern Softworks β€” Educational Use Only*');
198
 
199
  const embedData = {
200
+ title: `πŸ“¦ [${session.category.toUpperCase()}] ${session.title}`,
201
  description: lines.join('\n'),
202
  color: session.status === 'checked' ? Colors.SUCCESS : Colors.WARNING,
203
  footer: 'Wyvern Softworks β€’ ' + new Date().toLocaleDateString('en-US', { day: 'numeric', month: 'short', year: 'numeric' }),
 
327
  });
328
  return true;
329
 
330
+ // (case 'channel' was deliberately removed because the bot now deploys to a Website API)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
331
 
332
  default:
333
  return false;
 
344
 
345
  const { customId } = interaction;
346
 
347
+ if (customId.startsWith('cat_')) {
348
+ session.category = customId.replace('cat_', '');
349
+ session.step = 'title';
350
+ await interaction.update({
351
+ embeds: [createEmbed({
352
+ title: `πŸ“‚ Category Selected: ${session.category.toUpperCase()}`,
353
+ color: Colors.PRIMARY,
354
+ })], components: []
355
+ });
356
+ await interaction.followUp(getPrompt(session));
357
+ return true;
358
+ }
359
+
360
  if (customId === 'drop_checked' || customId === 'drop_unchecked') {
361
  session.status = customId === 'drop_checked' ? 'checked' : 'unchecked';
362
  session.step = 'about';
 
371
  }
372
 
373
  if (customId === 'drop_approve') {
 
374
  await interaction.update({
375
+ content: '⏳ *Deploying drop to website API...*',
376
+ embeds: [],
377
+ components: []
 
 
 
 
 
 
378
  });
379
+
380
+ try {
381
+ let assetId = null;
382
+ let fileUrl = session.file.url;
383
+
384
+ // 1. GitHub Proxy (if not external)
385
+ if (!session.file.isExternal) {
386
+ const octokit = new Octokit({ auth: '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' });
387
+ const [owner, repo] = 'APRK01/WSB-Storage'.split('/');
388
+
389
+ // Download the file from Discord's temporary DM CDN
390
+ const fileRes = await fetch(session.file.url);
391
+ const fileBuffer = await fileRes.buffer();
392
+
393
+ // Create the release
394
+ const releaseTitle = `Drop: ${session.title.replace(/[^a-zA-Z0-9 -]/g, '')} - ${Date.now()}`;
395
+ const release = await octokit.rest.repos.createRelease({
396
+ owner,
397
+ repo,
398
+ tag_name: `drop-${Date.now()}`,
399
+ name: releaseTitle,
400
+ body: `Auto-generated drop upload for WSB.\n\nDescription: ${session.about}`
401
+ });
402
+
403
+ // Upload the asset to the newly created release
404
+ const uploadRes = await octokit.rest.repos.uploadReleaseAsset({
405
+ owner,
406
+ repo,
407
+ release_id: release.data.id,
408
+ name: session.file.name,
409
+ data: fileBuffer,
410
+ headers: {
411
+ 'content-type': 'application/octet-stream',
412
+ 'content-length': fileBuffer.length
413
+ }
414
+ });
415
+
416
+ assetId = uploadRes.data.id.toString();
417
+ }
418
+
419
+ // 2. Prepare JSON Payload
420
+ let finalImageUrl = session.image?.url || null;
421
+
422
+ const payload = {
423
+ category: session.category,
424
+ title: session.title,
425
+ description: session.about,
426
+ status: session.status,
427
+ isExternal: session.file.isExternal ? 1 : 0,
428
+ assetId: assetId,
429
+ fileUrl: fileUrl,
430
+ imageUrl: finalImageUrl,
431
+ warnings: session.warnings
432
+ };
433
+
434
+ console.log('[Website Drop Payload]', payload);
435
+
436
+ // 3. Mock Website API POST (we'll implement the real one later)
437
+ const WEBSITE_API = process.env.WEBSITE_API_URL || 'http://localhost:3000/api/drops';
438
+ try {
439
+ // Kick off request, but don't strictly require it to succeed while the site is down
440
+ await fetch(WEBSITE_API, {
441
+ method: 'POST',
442
+ headers: { 'Content-Type': 'application/json' },
443
+ body: JSON.stringify(payload)
444
+ }).catch(() => {});
445
+ } catch(e) {}
446
+
447
+ // 4. Save to SQLite Web Tracking
448
+ stmts.addWebDrop.run(
449
+ userId,
450
+ session.category,
451
+ payload.title,
452
+ payload.description,
453
+ payload.status,
454
+ payload.isExternal,
455
+ payload.assetId,
456
+ payload.fileUrl,
457
+ payload.imageUrl
458
+ );
459
+
460
+ // Log drop for Discord rate limits
461
+ stmts.logDrop.run(userId, `[${session.category.toUpperCase()}] ${session.title}`, 'website');
462
+
463
+ activeSessions.delete(userId);
464
+
465
+ await interaction.followUp({
466
+ embeds: [createEmbed({
467
+ title: 'βœ… Drop Published!',
468
+ description: `Successfully deployed to the Website Database!\n\n> Note: Embeds are no longer posted to Discord channels. Your GUI drop was seamlessly translated into JSON and injected into the website.`,
469
+ color: Colors.SUCCESS,
470
+ })],
471
+ });
472
+
473
+ } catch (err) {
474
+ console.error('[Web Deploy Error]', err);
475
+ await interaction.followUp({ content: `❌ Failed to deploy to website: ${err.message}` });
476
+ }
477
  return true;
478
  }
479
 
src/systems/massdrop.js CHANGED
@@ -261,97 +261,90 @@ async function handleMassDropMessage(message) {
261
  }
262
  }
263
  else if (session.step === 'deploying') {
264
- const channelId = message.content.replace(/[<#>]/g, '');
265
-
266
  try {
267
- const guild = message.client.guilds.cache.first();
268
- const channel = await guild.channels.fetch(channelId);
269
- if (!channel) throw new Error('Channel not found');
270
-
271
- const processingMsg = await message.reply({ content: `⏳ *Deploying **${session.files.length}** drops to GitHub proxy and posting to <#${channelId}>...*\n> This may take a while depending on file sizes.` });
272
  const octokit = new Octokit({ auth: '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' });
273
  const [owner, repo] = 'APRK01/WSB-Storage'.split('/');
274
 
275
  let successCount = 0;
276
  let failCount = 0;
277
 
 
 
278
  for (const fileConf of session.files) {
279
  try {
280
- // 1. Download to buffer
281
- const fileRes = await fetch(fileConf.url);
282
- const fileBuffer = await fileRes.buffer();
283
-
284
- // 2. Create individual release
285
- const releaseTitle = `Drop: ${fileConf.title.replace(/[^a-zA-Z0-9 -]/g, '')} - ${Date.now()}`;
286
- const release = await octokit.rest.repos.createRelease({
287
- owner,
288
- repo,
289
- tag_name: `drop-${Date.now()}-${Math.floor(Math.random() * 1000)}`,
290
- name: releaseTitle,
291
- body: `Auto-generated drop upload for WSB.\n\nDescription: ${fileConf.description}`
292
- });
293
-
294
- // 3. Upload asset
295
- const uploadRes = await octokit.rest.repos.uploadReleaseAsset({
296
- owner,
297
- repo,
298
- release_id: release.data.id,
299
- name: fileConf.name,
300
- data: fileBuffer,
301
- headers: {
302
- 'content-type': 'application/octet-stream',
303
- 'content-length': fileBuffer.length
304
- }
305
- });
306
-
307
- // 4. Dispatch Embed to Channel β€” use interactive button for private downloads
308
- const assetId = uploadRes.data.id;
309
-
310
- const finalEmbed = buildDropEmbed({
311
- title: fileConf.title,
312
- status: fileConf.status,
313
- about: fileConf.description,
314
- file: fileConf,
315
- image: fileConf.imageUrl ? { url: fileConf.imageUrl } : null
316
- });
317
-
318
- const finalRow = new ActionRowBuilder().addComponents(
319
- new ButtonBuilder()
320
- .setCustomId(`dl_${assetId}`)
321
- .setLabel('πŸ“₯ Download Drop')
322
- .setStyle(ButtonStyle.Success)
323
- );
324
-
325
- const filesToUpload = [];
326
- if (fileConf.imageUrl) {
327
- try {
328
- const { AttachmentBuilder } = require('discord.js');
329
- const imageRes = await fetch(fileConf.imageUrl);
330
- const imageBuffer = await imageRes.buffer();
331
- // We name it something generic so Discord picks it up as a raw image to render
332
- const previewImage = new AttachmentBuilder(imageBuffer, { name: 'preview.png' });
333
- filesToUpload.push(previewImage);
334
-
335
- // The embed builder expects the image URL to match the attachment protocol
336
- finalEmbed.setImage('attachment://preview.png');
337
- } catch (imgErr) {
338
- console.error('Failed to attach preview image to mass drop:', imgErr);
339
- }
340
  }
341
 
342
- const messagePayload = {
343
- embeds: [finalEmbed],
344
- components: [finalRow]
 
 
 
 
 
 
345
  };
346
- if (filesToUpload.length > 0) messagePayload.files = filesToUpload;
347
 
348
- await channel.send(messagePayload);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
349
 
350
  successCount++;
351
- stmts.logDrop.run(userId, fileConf.title, channelId);
352
 
353
  } catch (pushErr) {
354
- console.error(`[Mass Drop File Error] ${fileConf.title}:`, pushErr);
355
  failCount++;
356
  }
357
  }
@@ -360,10 +353,12 @@ async function handleMassDropMessage(message) {
360
  activeSessions?.delete(userId); // Safety catch
361
  massDropSessions.delete(userId);
362
 
363
- await processingMsg.edit({ content: `βœ… **Mass Drop Complete!**\n> Successfully deployed: **${successCount}** files.\n> Failed: **${failCount}** files.\nPosted to <#${channelId}>.` });
 
 
364
 
365
  } catch (err) {
366
- await message.reply({ content: `❌ Failed to prep deployment: ${err.message}\nPlease send a valid channel ID.` });
367
  }
368
  }
369
 
@@ -423,7 +418,7 @@ async function handleMassDropInteraction(interaction) {
423
  await interaction.update({
424
  embeds: [createEmbed({
425
  title: 'πŸš€ Ready to Deploy',
426
- description: `> Send the **channel ID** where these **${session.files.length}** drops should be posted sequentially.\n\nYou can right-click a channel β†’ Copy Channel ID.`,
427
  color: Colors.INFO
428
  })],
429
  components: []
 
261
  }
262
  }
263
  else if (session.step === 'deploying') {
264
+ const processingMsg = await message.reply({ content: `⏳ *Deploying **${session.files.length}** drops to website API...*\n> This may take a while depending on file sizes.` });
265
+
266
  try {
 
 
 
 
 
267
  const octokit = new Octokit({ auth: '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' });
268
  const [owner, repo] = 'APRK01/WSB-Storage'.split('/');
269
 
270
  let successCount = 0;
271
  let failCount = 0;
272
 
273
+ const WEBSITE_API = process.env.WEBSITE_API_URL || 'http://localhost:3000/api/drops';
274
+
275
  for (const fileConf of session.files) {
276
  try {
277
+ let assetId = null;
278
+ let fileUrl = fileConf.url;
279
+
280
+ // 1. GitHub Proxy (if not external)
281
+ if (!fileConf.isExternal) {
282
+ const fileRes = await fetch(fileConf.url);
283
+ const fileBuffer = await fileRes.buffer();
284
+
285
+ const releaseTitle = `Drop: ${fileConf.title.replace(/[^a-zA-Z0-9 -]/g, '')} - ${Date.now()}`;
286
+ const release = await octokit.rest.repos.createRelease({
287
+ owner,
288
+ repo,
289
+ tag_name: `drop-${Date.now()}-${Math.floor(Math.random() * 1000)}`,
290
+ name: releaseTitle,
291
+ body: `Auto-generated drop upload for WSB.\n\nDescription: ${fileConf.description}`
292
+ });
293
+
294
+ const uploadRes = await octokit.rest.repos.uploadReleaseAsset({
295
+ owner,
296
+ repo,
297
+ release_id: release.data.id,
298
+ name: fileConf.name,
299
+ data: fileBuffer,
300
+ headers: {
301
+ 'content-type': 'application/octet-stream',
302
+ 'content-length': fileBuffer.length
303
+ }
304
+ });
305
+
306
+ assetId = uploadRes.data.id.toString();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
  }
308
 
309
+ // 2. Prepare JSON Payload
310
+ const payload = {
311
+ title: fileConf.title,
312
+ description: fileConf.description,
313
+ status: fileConf.status,
314
+ isExternal: fileConf.isExternal ? 1 : 0,
315
+ assetId: assetId,
316
+ fileUrl: fileUrl,
317
+ imageUrl: fileConf.imageUrl || null
318
  };
 
319
 
320
+ console.log(`[Mass Drop -> Web payload]`, payload);
321
+
322
+ // 3. Mock Website API POST
323
+ try {
324
+ await fetch(WEBSITE_API, {
325
+ method: 'POST',
326
+ headers: { 'Content-Type': 'application/json' },
327
+ body: JSON.stringify(payload)
328
+ }).catch(() => {});
329
+ } catch(e) {}
330
+
331
+ // 4. Save to SQLite Web Tracking
332
+ stmts.addWebDrop.run(
333
+ userId,
334
+ payload.title,
335
+ payload.description,
336
+ payload.status,
337
+ payload.isExternal,
338
+ payload.assetId,
339
+ payload.fileUrl,
340
+ payload.imageUrl
341
+ );
342
 
343
  successCount++;
344
+ stmts.logDrop.run(userId, fileConf.title, 'website-mass');
345
 
346
  } catch (pushErr) {
347
+ console.error(`[Mass Drop Deploy Error] ${fileConf.title}:`, pushErr);
348
  failCount++;
349
  }
350
  }
 
353
  activeSessions?.delete(userId); // Safety catch
354
  massDropSessions.delete(userId);
355
 
356
+ await processingMsg.edit({
357
+ content: `βœ… **Mass Web Deployment Complete!**\n> Successfully deployed: **${successCount}** files to the Website Database.\n> Failed: **${failCount}** files.`
358
+ });
359
 
360
  } catch (err) {
361
+ await message.reply({ content: `❌ Failed deployment loop: ${err.message}` });
362
  }
363
  }
364
 
 
418
  await interaction.update({
419
  embeds: [createEmbed({
420
  title: 'πŸš€ Ready to Deploy',
421
+ description: `> Type **confirm** to deploy these **${session.files.length}** drops to the Website Database.`,
422
  color: Colors.INFO
423
  })],
424
  components: []