adds user accounts, service requests, dashboard, admin panel, better layout, db+altcha+auth support
This commit is contained in:
parent
dfbc3cade9
commit
0043a5bf3c
40 changed files with 3981 additions and 188 deletions
266
app/dashboard/page.tsx
Normal file
266
app/dashboard/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue