Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
716 changes: 715 additions & 1 deletion package-lock.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Session" ADD COLUMN "access_token" TEXT;
13 changes: 7 additions & 6 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,13 @@ model ErrorReport {
/// Sessions for web authentication. Stores a hash of the session token
/// (so the raw token is only sent to the user's cookie) and points to a User.
model Session {
id String @id @default(cuid())
hashedId String @unique
user_id String @db.VarChar(20)
user_json Json
created_at DateTime @default(now())
expires_at DateTime
id String @id @default(cuid())
hashedId String @unique
user_id String @db.VarChar(20)
user_json Json
access_token String? @db.Text
created_at DateTime @default(now())
expires_at DateTime

user User @relation(fields: [user_id], references: [id])
}
24 changes: 24 additions & 0 deletions web/app/(webapp)/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// This file is a part of AlphaGameBot.
//
// AlphaGameBot - A Discord bot that's free and (hopefully) doesn't suck.
// Copyright (C) 2025 Damien Boisvert (AlphaGameDeveloper)
//
// AlphaGameBot is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// AlphaGameBot is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with AlphaGameBot. If not, see <https://www.gnu.org/licenses/>.

export default function WebAppRoot() {
return <>
<h1 className="text-4xl font-bold">Howdy, there!</h1>
<p className="mt-4 text-lg">Welcome to the AlphaGameBot Web App. More features coming soon!</p>
</>;
}
172 changes: 172 additions & 0 deletions web/app/(webapp)/app/guilds/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// This file is a part of AlphaGameBot.
//
// AlphaGameBot - A Discord bot that's free and (hopefully) doesn't suck.
// Copyright (C) 2025 Damien Boisvert (AlphaGameDeveloper)
//
// AlphaGameBot is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// AlphaGameBot is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with AlphaGameBot. If not, see <https://www.gnu.org/licenses/>.

"use client";

import { useEffect, useState } from "react";

interface Guild {
id: string;
name: string;
icon: string | null;
hasBot: boolean;
dbInfo?: {
id: string;
name: string;
updated_at: Date;
};
}

export default function WebAppPage() {
const [guilds, setGuilds] = useState<Guild[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
fetch('/api/guilds')
.then((res) => {
if (!res.ok) throw new Error('Failed to fetch guilds');
return res.json();
})
.then((data) => {
setGuilds(data.guilds || []);
setLoading(false);
})
.catch((err) => {
setError(err.message);
setLoading(false);
});
}, []);

if (loading) {
return (
<div className="text-center py-12">
<div className="w-12 h-12 border-4 border-t-transparent rounded-full animate-spin mx-auto mb-4"
style={{ borderColor: 'var(--primary-500)', borderTopColor: 'transparent' }}></div>
<p style={{ color: 'var(--text-muted)' }}>Loading your servers...</p>
</div>
);
}

if (error) {
return (
<div className="text-center py-12">
<div className="text-red-500 mb-4">
<svg className="w-16 h-16 mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<p className="text-lg font-medium">Error loading servers</p>
<p className="text-sm" style={{ color: 'var(--text-muted)' }}>{error}</p>
</div>
</div>
);
}

if (guilds.length === 0) {
return (
<div className="text-center py-12">
<div className="mb-4" style={{ color: 'var(--text-muted)' }}>
<svg className="w-16 h-16 mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
<p className="text-lg font-medium" style={{ color: 'var(--text-default)' }}>No servers found</p>
<p className="text-sm mt-2">You don't have any servers where you're an administrator and AlphaGameBot is present.</p>
<div className="mt-6">
<a
href={`https://discord.com/oauth2/authorize?client_id=${process.env.NEXT_PUBLIC_DISCORD_CLIENT_ID || process.env.DISCORD_CLIENT_ID}&permissions=8&scope=bot`}
target="_blank"
rel="noopener noreferrer"
className="inline-block px-6 py-3 rounded font-medium transition"
style={{ background: 'var(--primary-500)', color: 'white' }}
>
Invite AlphaGameBot
</a>
</div>
</div>
</div>
);
}

return (
<div>
<div className="mb-6">
<h1 className="text-3xl font-bold mb-2">Your Servers</h1>
<p style={{ color: 'var(--text-muted)' }}>
Manage AlphaGameBot in servers where you have administrator permissions
</p>
</div>

<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{guilds.map((guild) => (
<div
key={guild.id}
className="rounded-lg border p-6 transition hover:border-opacity-50 cursor-pointer"
style={{
background: 'var(--surface-100)',
borderColor: 'var(--border)',
}}
>
<div className="flex items-center gap-4 mb-4">
{guild.icon ? (
<img
src={`https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.${guild.icon.startsWith("a_") ? "gif" : "png"}?size=64`}
alt={`${guild.name} icon`}
className="w-16 h-16 rounded-full"
/>
) : (
<div
className="w-16 h-16 rounded-full flex items-center justify-center text-2xl font-bold"
style={{ background: 'var(--surface-200)', color: 'var(--primary-500)' }}
>
{guild.name.charAt(0).toUpperCase()}
</div>
)}
<div className="flex-1 min-w-0">
<h3 className="text-lg font-semibold truncate">{guild.name}</h3>
<p className="text-sm" style={{ color: 'var(--text-muted)' }}>
ID: {guild.id}
</p>
</div>
</div>

<div className="flex gap-2">
<button
className="flex-1 px-4 py-2 rounded font-medium transition"
style={{
background: 'var(--primary-500)',
color: 'white',
}}
>
Configure
</button>
<button
className="px-4 py-2 rounded border transition"
style={{
borderColor: 'var(--border)',
color: 'var(--text-muted)',
}}
>
Stats
</button>
</div>
</div>
))}
</div>
</div>
);
}
153 changes: 153 additions & 0 deletions web/app/(webapp)/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// This file is a part of AlphaGameBot.
//
// AlphaGameBot - A Discord bot that's free and (hopefully) doesn't suck.
// Copyright (C) 2025 Damien Boisvert (AlphaGameDeveloper)
//
// AlphaGameBot is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// AlphaGameBot is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with AlphaGameBot. If not, see <https://www.gnu.org/licenses/>.

"use client";

import Link from "next/link";
import { usePathname } from "next/navigation";
import { useEffect, useState } from "react";
import type { User } from "discord.js";

export default function appLayout({
children,
}: {
children: React.ReactNode;
}) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const pathname = usePathname();

useEffect(() => {
fetch('/api/auth/session')
.then((res) => res.json())
.then((data) => {
if (!data.user) {
window.location.href = '/api/auth/login';
} else {
setUser(data.user);
setLoading(false);
}
})
.catch(() => {
window.location.href = '/api/auth/login';
});
}, []);

if (loading) {
return (
<div className="flex items-center justify-center min-h-screen" style={{ background: 'var(--bg)' }}>
<div className="text-center">
<div className="w-16 h-16 border-4 border-t-transparent rounded-full animate-spin mx-auto mb-4"
style={{ borderColor: 'var(--primary-500)', borderTopColor: 'transparent' }}></div>
<p style={{ color: 'var(--text-muted)' }}>Loading...</p>
</div>
</div>
);
}

return (
<div className="flex flex-col min-h-screen" style={{ background: 'var(--bg)', color: 'var(--text-default)' }}>
{/* Top Bar */}
<header className="border-b" style={{
background: 'var(--surface-100)',
borderColor: 'var(--border)'
}}>
<div className="container mx-auto px-4 py-4 flex items-center justify-between">
<div className="flex items-center gap-4">
<Link href="/" className="text-xl font-bold">
AlphaGame<span style={{ color: 'var(--primary-500)' }}>Bot</span>
</Link>
<span style={{ color: 'var(--text-muted)' }}>/</span>
<span style={{ color: 'var(--text-muted)' }}>Dashboard</span>
</div>

{user && (
<div className="flex items-center gap-3">
<img
src={
user.avatar
? `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.${user.avatar.startsWith("a_") ? "gif" : "png"}?size=64`
: `https://cdn.discordapp.com/embed/avatars/${Number(user.discriminator) % 5}.png`
}
alt={`${user.username} avatar`}
className="w-10 h-10 rounded-full border"
style={{ borderColor: 'var(--border)' }}
/>
<div className="hidden md:block">
<div className="text-sm font-medium">{user.username}</div>
<div className="text-xs" style={{ color: 'var(--text-muted)' }}>
#{user.discriminator}
</div>
</div>
</div>
)}
</div>
</header>

<div className="flex flex-1">
{/* Right Side Navigation */}
<nav className="w-64 border-r p-6" style={{
background: 'var(--surface-100)',
borderColor: 'var(--border)'
}}>
<ul className="space-y-2">
<li>
<Link
href="/app"
className={`block px-4 py-2 rounded transition ${
pathname === '/app'
? 'font-medium'
: ''
}`}
style={{
background: pathname === '/app/guilds' ? 'var(--primary-500)' : 'transparent',
color: pathname === '/app/guilds' ? 'white' : 'var(--text-default)',
}}
>
Servers
</Link>
</li>
<li>
<a
href="/"
className="block px-4 py-2 rounded transition"
style={{ color: 'var(--text-muted)' }}
>
Back to Website
</a>
</li>
<li>
<a
href="/api/auth/logout"
className="block px-4 py-2 rounded transition"
style={{ color: 'var(--danger-500)' }}
>
Sign Out
</a>
</li>
</ul>
</nav>

{/* Main Content */}
<main className="flex-1 p-6">
{children}
</main>
</div>
</div>
);
}
Loading
Loading