adds user accounts, service requests, dashboard, admin panel, better layout, db+altcha+auth support

This commit is contained in:
Aidan 2025-07-07 20:01:59 -04:00
parent dfbc3cade9
commit 0043a5bf3c
40 changed files with 3981 additions and 188 deletions

266
app/dashboard/page.tsx Normal file
View file

@ -0,0 +1,266 @@
"use client"
import { Nav } from "@/components/core/nav";
import { authClient } from "@/util/auth-client";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { SiForgejo, SiJellyfin, SiOllama } from "react-icons/si";
import {
TbDashboard,
TbUser,
TbMail,
TbCalendar,
TbShield,
TbDeviceTv,
TbExternalLink,
TbServer,
TbCheck,
TbReceipt,
} from "react-icons/tb";
export default function Dashboard() {
const router = useRouter();
const { data: session, isPending } = authClient.useSession();
const [mounted, setMounted] = useState(false);
const [userServices, setUserServices] = useState<string[]>([]);
const [openServices, setOpenServices] = useState<string[]>([]);
useEffect(() => {
setMounted(true);
}, []);
useEffect(() => {
if (mounted && !isPending && !session) {
router.push("/login?message=Please sign in to access the dashboard");
}
}, [session, isPending, mounted, router]);
useEffect(() => {
if (session) {
fetchUserServices();
}
}, [session]);
const fetchUserServices = async () => {
try {
const response = await fetch("/api/user-services");
if (response.ok) {
const data = await response.json();
const services = data.services;
setUserServices(services.map((s: { serviceName: string }) => s.serviceName));
setOpenServices(services.filter((s: { isOpen: boolean }) => s.isOpen).map((s: { serviceName: string }) => s.serviceName));
}
} catch (error) {
console.error("Error fetching services:", error);
}
};
if (!mounted || isPending) {
return (
<main>
<Nav />
<div className="flex flex-col items-center justify-center min-h-[50vh] px-4">
<div className="animate-pulse text-lg">loading dashboard...</div>
</div>
</main>
);
}
if (!session) {
return (
<main>
<Nav />
<div className="flex flex-col items-center justify-center min-h-[50vh] px-4">
<div className="text-lg">redirecting to login...</div>
</div>
</main>
);
}
const formatDate = (date: Date) => {
return new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
}).format(date);
};
return (
<main>
<Nav />
<div className="max-w-4xl mx-auto px-4 py-8">
<div className="flex flex-row items-center justify-start gap-3 mb-8">
<TbDashboard size={32} className="text-blue-500" />
<h1 className="text-3xl sm:text-4xl font-bold">
Dashboard
</h1>
</div>
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 p-6 rounded-2xl mb-8">
<h2 className="text-2xl font-semibold">
Welcome back, {session.user.name || 'User'}! 👋
</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<ServiceCard
title="TV"
icon={<TbDeviceTv />}
link="https://tv.ihate.college"
signupType="request"
signupLink="https://t.me/p0ntus"
hasAccess={userServices.includes("tv")}
isOpen={openServices.includes("tv")}
/>
<ServiceCard
title="TV Requests"
icon={<SiJellyfin />}
link="https://requests.ihate.college"
signupType="request"
signupLink="https://t.me/p0ntus"
hasAccess={userServices.includes("tv")}
isOpen={openServices.includes("tv")}
/>
<ServiceCard
title="Git"
icon={<SiForgejo />}
link="https://git.p0ntus.com"
signupType="self"
signupLink="https://git.p0ntus.com/user/sign_up"
hasAccess={userServices.includes("git")}
isOpen={openServices.includes("git")}
/>
<ServiceCard
title="Mail"
icon={<TbMail />}
link="https://pontusmail.org"
signupType="self"
signupLink="https://pontusmail.org/admin/user/signup"
hasAccess={userServices.includes("mail")}
isOpen={openServices.includes("mail")}
/>
<ServiceCard
title="AI"
icon={<SiOllama />}
link="https://ai.ihate.college"
signupType="request"
signupLink="https://t.me/p0ntus"
hasAccess={userServices.includes("ai")}
isOpen={openServices.includes("ai")}
/>
<ServiceCard
title="Hosting"
icon={<TbServer />}
signupType="request"
signupLink="https://t.me/p0ntus"
hasAccess={userServices.includes("hosting")}
isOpen={openServices.includes("hosting")}
/>
</div>
<div className="bg-white dark:bg-gray-800 p-6 rounded-xl justify-between shadow-sm border border-gray-200 dark:border-gray-700 flex flex-row items-center gap-3 mb-8">
<h3 className="text-xl font-semibold">Need access to a service?</h3>
<Link href="/requests" className="bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600 transition-colors flex items-center gap-2 cursor-pointer">
<TbReceipt className="w-4 h-4" />
Make a Request
</Link>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-white dark:bg-gray-800 p-6 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700">
<div className="flex items-center gap-3 mb-4">
<TbUser className="text-blue-500 w-6 h-6" />
<h3 className="text-xl font-semibold">My Profile</h3>
</div>
<div className="space-y-3">
<div className="flex items-center gap-2">
<TbUser className="w-4 h-4 text-gray-400" />
<span className="text-sm text-gray-500">Name:</span>
<span className="font-medium">{session.user.name || 'Not provided'}</span>
</div>
<div className="flex items-center gap-2">
<TbMail className="w-4 h-4 text-gray-400" />
<span className="text-sm text-gray-500">Email:</span>
<span className="font-medium">{session.user.email}</span>
</div>
<div className="flex items-center gap-2">
<TbShield className="w-4 h-4 text-gray-400" />
<span className="text-sm text-gray-500">Email Verified:</span>
<span className={`font-medium ${session.user.emailVerified ? 'text-green-600' : 'text-orange-500'}`}>
{session.user.emailVerified ? 'Yes' : 'No'}
</span>
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 p-6 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700">
<div className="flex items-center gap-3 mb-4">
<TbCalendar className="text-green-500 w-6 h-6" />
<h3 className="text-xl font-semibold">Account Details</h3>
</div>
<div className="space-y-3">
<div className="flex items-center gap-2">
<TbCalendar className="w-4 h-4 text-gray-400" />
<span className="text-sm text-gray-500">Account Created:</span>
<span className="font-medium text-sm">{formatDate(session.user.createdAt)}</span>
</div>
<div className="flex items-center gap-2">
<TbCalendar className="w-4 h-4 text-gray-400" />
<span className="text-sm text-gray-500">Last Updated:</span>
<span className="font-medium text-sm">{formatDate(session.user.updatedAt)}</span>
</div>
</div>
</div>
</div>
</div>
</main>
);
}
function ServiceCard({ title, icon, link, signupType, signupLink, hasAccess, isOpen }: {
title: string,
icon: React.ReactNode,
link?: string,
signupType: "request" | "self" | "invite",
signupLink: string,
hasAccess: boolean,
isOpen: boolean
}) {
const cardClassName = hasAccess
? "bg-green-50 dark:bg-green-900/20 p-6 rounded-xl shadow-sm border border-green-200 dark:border-green-700"
: "bg-red-50 dark:bg-red-900/20 p-6 rounded-xl shadow-sm border border-red-200 dark:border-red-700";
return (
<div className={cardClassName}>
<div className="flex items-center justify-between gap-3 mb-4">
<div className="flex items-center gap-2 cursor-pointer" onClick={() => hasAccess && link && window.open(link, '_blank')}>
{icon}
<h3 className="text-xl font-semibold">{title}</h3>
</div>
{hasAccess && link && <div className="flex items-center gap-2">
<TbExternalLink className="text-blue-500 w-6 h-6 cursor-pointer" onClick={() => window.open(link, '_blank')} />
</div>}
</div>
{hasAccess ? (
<div className="space-y-2">
<div className="flex items-center gap-2 text-green-600 dark:text-green-400">
<TbCheck className="w-4 h-4" />
<span className="text-sm font-medium">Access granted</span>
</div>
{isOpen && (
<p className="text-sm text-gray-500">
Open service: <Link href={signupLink} className="text-blue-500 hover:underline">{signupType === "request" ? "Request account" : "Create account"}</Link>
</p>
)}
</div>
) : (
<p className="text-sm text-gray-500 mt-4">
Need an account? <Link href={signupLink} className="text-blue-500 hover:underline">{signupType === "request" ? "Request one!" : "Sign up!"}</Link>
</p>
)}
</div>
);
}