feat: ui/ux improvements, updates, + update dependencies

- new mobile menu
- better display and viewing for service cards
- nicer request layout
- should exit after seeding db!
- add missing link for pass
- chat viewing by admins has been disabled for open webui
This commit is contained in:
Aidan 2025-07-24 20:45:50 -07:00
parent 647932b76f
commit 59f9c709ce
9 changed files with 342 additions and 211 deletions

View file

@ -67,7 +67,7 @@ export default function About() {
</div> </div>
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 my-3"> <div className="flex flex-col sm:flex-row items-center justify-center gap-4 my-3">
<Link href="https://t.me/p0ntu5"> <Link href="https://t.me/p0ntu5">
<button className="flex flex-row items-center justify-center gap-2 bg-blue-500 text-white px-4 py-2 rounded-md w-full sm:w-auto"> <button className="flex flex-row items-center justify-center gap-2 bg-blue-500 text-white px-4 py-2 rounded-md w-full sm:w-auto cursor-pointer">
<RiTelegram2Line size={24} /> <RiTelegram2Line size={24} />
<span className="text-sm sm:text-base"> <span className="text-sm sm:text-base">
contact contact
@ -75,7 +75,7 @@ export default function About() {
</button> </button>
</Link> </Link>
<Link href="https://t.me/pontushub"> <Link href="https://t.me/pontushub">
<button className="flex flex-row items-center justify-center gap-2 bg-blue-500 text-white px-4 py-2 rounded-md w-full sm:w-auto"> <button className="flex flex-row items-center justify-center gap-2 bg-blue-500 text-white px-4 py-2 rounded-md w-full sm:w-auto cursor-pointer">
<RiTelegram2Line size={24} /> <RiTelegram2Line size={24} />
<span className="text-sm sm:text-base"> <span className="text-sm sm:text-base">
join channel join channel

View file

@ -11,7 +11,7 @@ async function getChallenge() {
const challenge = await createChallenge({ const challenge = await createChallenge({
hmacKey, hmacKey,
maxNumber: 1400000, maxNumber: 1000000,
}) })
return NextResponse.json(challenge) return NextResponse.json(challenge)

View file

@ -77,14 +77,21 @@ export default function Home() {
<h2 className="text-2xl sm:text-3xl font-bold text-center w-full">Services</h2> <h2 className="text-2xl sm:text-3xl font-bold text-center w-full">Services</h2>
<h3 className="text-lg sm:text-xl italic text-center w-full text-gray-600 hidden sm:block">what can we offer you?</h3> <h3 className="text-lg sm:text-xl italic text-center w-full text-gray-600 hidden sm:block">what can we offer you?</h3>
{loading ? ( {loading ? (
<div className="animate-pulse text-lg">Loading services...</div> <div className="grid grid-cols-3 gap-8 sm:gap-10 my-6 sm:my-8">
{[...Array(6)].map((_, index) => (
<div key={index} className="flex flex-col items-center justify-center gap-3 animate-pulse">
<div className="w-10 h-10 sm:w-12 sm:h-12 bg-gray-200 dark:bg-gray-700 rounded-lg"></div>
<div className="w-16 h-4 sm:h-5 bg-gray-200 dark:bg-gray-700 rounded"></div>
</div>
))}
</div>
) : ( ) : (
<div className="grid grid-cols-2 sm:grid-cols-3 gap-8 sm:gap-10 my-6 sm:my-8"> <div className="grid grid-cols-3 gap-8 sm:gap-10 my-6 sm:my-8">
{services.map((service) => { {services.map((service) => {
const IconComponent = getServiceIcon(service.name); const IconComponent = getServiceIcon(service.name);
return ( return (
<div key={service.id} className="flex flex-col items-center justify-center gap-3"> <div key={service.id} className="flex flex-col items-center justify-center gap-3">
<Link href={`/services/${service.name}`} className="flex flex-col items-center gap-2 hover:opacity-75 transition-opacity"> <Link href={`/services/${service.name}`} className="flex flex-col items-center gap-2 hover:opacity-75 transition-opacity duration-200">
<IconComponent size={40} className="sm:w-12 sm:h-12" /> <IconComponent size={40} className="sm:w-12 sm:h-12" />
<h3 className="text-base sm:text-lg font-bold">{service.name}</h3> <h3 className="text-base sm:text-lg font-bold">{service.name}</h3>
</Link> </Link>
@ -96,14 +103,14 @@ export default function Home() {
</div> </div>
<div className="flex flex-col items-center justify-start gap-6 w-full lg:max-w-md"> <div className="flex flex-col items-center justify-start gap-6 w-full lg:max-w-md">
<h2 className="text-2xl sm:text-3xl font-bold text-center w-full">Where we are</h2> <h2 className="text-2xl sm:text-3xl font-bold text-center w-full">Where we operate</h2>
<h3 className="text-lg sm:text-xl italic text-center w-full text-gray-600 hidden sm:block">how can you find us?</h3> <h3 className="text-lg sm:text-xl italic text-center w-full text-gray-600 hidden sm:block">how can you find us?</h3>
<div className="flex flex-col items-center gap-6 mt-6"> <div className="flex flex-col items-center gap-6 mt-6">
<p className="text-base sm:text-lg text-center"> <p className="text-base sm:text-lg text-center">
p0ntus is fully on the public internet! our servers are mainly located in the united states. p0ntus operates fully on the internet! our servers are mainly located in the united states.
</p> </p>
<p className="text-base sm:text-lg text-center"> <p className="text-base sm:text-lg text-center">
we also operate servers in the united states, canada and germany. we also operate servers located in canada and germany.
</p> </p>
<Link href="/servers" className="flex flex-row items-center gap-2 text-base sm:text-lg text-center text-blue-500 hover:underline transition-colors"> <Link href="/servers" className="flex flex-row items-center gap-2 text-base sm:text-lg text-center text-blue-500 hover:underline transition-colors">
our servers <TbArrowRight size={20} /> our servers <TbArrowRight size={20} />
@ -116,10 +123,10 @@ export default function Home() {
<h3 className="text-lg sm:text-xl italic text-center w-full text-gray-600 hidden sm:block">what&apos;s the point?</h3> <h3 className="text-lg sm:text-xl italic text-center w-full text-gray-600 hidden sm:block">what&apos;s the point?</h3>
<div className="flex flex-col items-center gap-6 mt-6"> <div className="flex flex-col items-center gap-6 mt-6">
<p className="text-base sm:text-lg text-center"> <p className="text-base sm:text-lg text-center">
everything today includes microtransactions, and we were fed up with it. everything today revolves around microtransactions, and we&apos;re fed up with it.
</p> </p>
<p className="text-base sm:text-lg text-center"> <p className="text-base sm:text-lg text-center">
p0ntus exists to show that it is possible to have a free and open set of services that people have fun using. p0ntus exists to show that it&apos;s possible to have a free and open set of services that people enjoy using.
</p> </p>
</div> </div>
</div> </div>

View file

@ -199,155 +199,157 @@ export default function ServiceRequests() {
return ( return (
<main> <main>
<Nav /> <Nav />
<div className="max-w-4xl mx-auto px-4 py-8"> <div className="max-w-6xl mx-auto px-4 py-8">
<div className="flex flex-row items-center justify-start gap-3 mb-8"> <div className="flex flex-row items-center justify-start gap-3 mb-8">
<TbSend size={32} className="text-blue-500" /> <TbSend size={32} className="text-blue-500" />
<h1 className="text-3xl sm:text-4xl font-bold">Service Requests</h1> <h1 className="text-3xl sm:text-4xl font-bold">Service Requests</h1>
</div> </div>
<div className="bg-white dark:bg-gray-800 p-6 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 mb-8"> <div className="grid grid-cols-1 md:grid-cols-2 md:gap-4">
<h2 className="text-xl font-semibold mb-4">Request Service Access</h2> <div className="bg-white dark:bg-gray-800 p-6 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 mb-8">
<div className="bg-blue-50 dark:bg-blue-900/20 p-4 rounded-lg border border-blue-200 dark:border-blue-700 mb-6"> <h2 className="text-xl font-semibold mb-4">Request Service Access</h2>
<div className="flex items-start gap-3"> <div className="bg-blue-50 dark:bg-blue-900/20 p-4 rounded-lg border border-blue-200 dark:border-blue-700 mb-6">
<TbInfoCircle className="w-5 h-5 text-blue-500 mt-0.5 flex-shrink-0" /> <div className="flex items-start gap-3">
<div> <TbInfoCircle className="w-5 h-5 text-blue-500 mt-0.5 flex-shrink-0" />
<p className="text-sm text-blue-700 dark:text-blue-300"> <div>
<strong>Note:</strong> Open services (like Git and Mail) don&apos;t require requests - you already have access! <p className="text-sm text-blue-700 dark:text-blue-300">
Visit the <Link href="/dashboard" className="underline hover:no-underline">dashboard</Link> to see your available services <strong>Note:</strong> Open services (like Git and Mail) don&apos;t require requests - you already have access!
and create accounts directly. Visit the <Link href="/dashboard" className="underline hover:no-underline">dashboard</Link> to see your available services
</p> and create accounts directly.
</p>
</div>
</div> </div>
</div> </div>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="service" className="block text-sm font-medium mb-2">
Service
</label>
<select
id="service"
value={selectedService}
onChange={(e) => setSelectedService(e.target.value)}
className="w-full p-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700"
required
>
<option value="">Select a service</option>
{services
.filter(s => (s.priceStatus === "by-request" || s.priceStatus === "invite-only") && !userServices.includes(s.name))
.map((service) => (
<option key={service.name} value={service.name}>
{service.name.charAt(0).toUpperCase() + service.name.slice(1)} - {service.description}
</option>
))}
</select>
{services.filter(s => (s.priceStatus === "by-request" || s.priceStatus === "invite-only") && !userServices.includes(s.name)).length === 0 && (
<p className="text-sm text-gray-500 mt-2">
No services require requests at this time. All available services are either open or you already have access.
</p>
)}
</div>
<div>
<label htmlFor="reason" className="block text-sm font-medium mb-2">
Reason for Request
</label>
<textarea
id="reason"
value={reason}
onChange={(e) => setReason(e.target.value)}
rows={4}
className="w-full p-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700"
placeholder="Please explain why you need access to this service..."
required
/>
</div>
<div>
<Altcha onStateChange={(ev) => {
if ('detail' in ev) {
setCaptchaToken(ev.detail.payload || "");
}
}} />
</div>
<button
type="submit"
disabled={submitting || !selectedService || !reason || !captchaToken}
className="bg-blue-500 text-white px-6 py-3 rounded-lg hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center gap-2"
>
<TbSend className="w-4 h-4" />
{submitting ? "Submitting..." : "Submit Request"}
</button>
</form>
{message && (
<div className={`mt-4 p-3 rounded-lg ${message.includes("success") ? "bg-green-50 text-green-700 border border-green-200" : "bg-red-50 text-red-700 border border-red-200"}`}>
{message}
</div>
)}
</div> </div>
<form onSubmit={handleSubmit} className="space-y-4"> <div className="bg-white dark:bg-gray-800 p-6 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 mb-8">
<div> <h2 className="text-xl font-semibold mb-4 flex items-center gap-2">
<label htmlFor="service" className="block text-sm font-medium mb-2"> <TbEye className="w-5 h-5" />
Service My Requests
</label> </h2>
<select
id="service"
value={selectedService}
onChange={(e) => setSelectedService(e.target.value)}
className="w-full p-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700"
required
>
<option value="">Select a service</option>
{services
.filter(s => (s.priceStatus === "by-request" || s.priceStatus === "invite-only") && !userServices.includes(s.name))
.map((service) => (
<option key={service.name} value={service.name}>
{service.name.charAt(0).toUpperCase() + service.name.slice(1)} - {service.description}
</option>
))}
</select>
{services.filter(s => (s.priceStatus === "by-request" || s.priceStatus === "invite-only") && !userServices.includes(s.name)).length === 0 && (
<p className="text-sm text-gray-500 mt-2">
No services require requests at this time. All available services are either open or you already have access.
</p>
)}
</div>
<div> {loading ? (
<label htmlFor="reason" className="block text-sm font-medium mb-2"> <div className="text-center py-8">
Reason for Request <div className="animate-pulse">Loading requests...</div>
</label> </div>
<textarea ) : requests.length === 0 ? (
id="reason" <div className="text-center py-8 text-gray-500">
value={reason} No requests found. Submit your first request above!
onChange={(e) => setReason(e.target.value)} </div>
rows={4} ) : (
className="w-full p-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700" <div className="space-y-4">
placeholder="Please explain why you need access to this service..." {requests.map((request) => (
required <div key={request.id} className="border border-gray-200 dark:border-gray-600 rounded-lg p-4">
/> <div className="flex items-start justify-between mb-2">
</div> <div>
<h3 className="font-semibold text-lg">
<div> {request.serviceName.charAt(0).toUpperCase() + request.serviceName.slice(1)}
<Altcha onStateChange={(ev) => { </h3>
if ('detail' in ev) { <p className="text-sm text-gray-600 dark:text-gray-400">
setCaptchaToken(ev.detail.payload || ""); {request.serviceDescription}
} </p>
}} /> </div>
</div> <div className={`flex items-center gap-2 px-3 py-1 rounded-full border ${getStatusColor(request.status)}`}>
{getStatusIcon(request.status)}
<button <span className="text-sm font-medium capitalize">{request.status}</span>
type="submit" </div>
disabled={submitting || !selectedService || !reason || !captchaToken}
className="bg-blue-500 text-white px-6 py-3 rounded-lg hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center gap-2"
>
<TbSend className="w-4 h-4" />
{submitting ? "Submitting..." : "Submit Request"}
</button>
</form>
{message && (
<div className={`mt-4 p-3 rounded-lg ${message.includes("success") ? "bg-green-50 text-green-700 border border-green-200" : "bg-red-50 text-red-700 border border-red-200"}`}>
{message}
</div>
)}
</div>
<div className="bg-white dark:bg-gray-800 p-6 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700">
<h2 className="text-xl font-semibold mb-4 flex items-center gap-2">
<TbEye className="w-5 h-5" />
My Requests
</h2>
{loading ? (
<div className="text-center py-8">
<div className="animate-pulse">Loading requests...</div>
</div>
) : requests.length === 0 ? (
<div className="text-center py-8 text-gray-500">
No requests found. Submit your first request above!
</div>
) : (
<div className="space-y-4">
{requests.map((request) => (
<div key={request.id} className="border border-gray-200 dark:border-gray-600 rounded-lg p-4">
<div className="flex items-start justify-between mb-2">
<div>
<h3 className="font-semibold text-lg">
{request.serviceName.charAt(0).toUpperCase() + request.serviceName.slice(1)}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">
{request.serviceDescription}
</p>
</div> </div>
<div className={`flex items-center gap-2 px-3 py-1 rounded-full border ${getStatusColor(request.status)}`}>
{getStatusIcon(request.status)} <div className="mb-3">
<span className="text-sm font-medium capitalize">{request.status}</span> <p className="text-sm text-gray-600 dark:text-gray-400 mb-1">Reason:</p>
<p className="text-sm">{request.reason}</p>
</div> </div>
</div>
<div className="mb-3"> {request.adminNotes && (
<p className="text-sm text-gray-600 dark:text-gray-400 mb-1">Reason:</p> <div className="mb-3 p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
<p className="text-sm">{request.reason}</p> <p className="text-sm text-gray-600 dark:text-gray-400 mb-1 flex items-center gap-1">
</div> <TbNotes className="w-4 h-4" />
Admin Notes:
{request.adminNotes && ( </p>
<div className="mb-3 p-3 bg-gray-50 dark:bg-gray-700 rounded-lg"> <p className="text-sm">{request.adminNotes}</p>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-1 flex items-center gap-1"> </div>
<TbNotes className="w-4 h-4" />
Admin Notes:
</p>
<p className="text-sm">{request.adminNotes}</p>
</div>
)}
<div className="text-xs text-gray-500">
Submitted: {new Date(request.createdAt).toLocaleDateString()}
{request.reviewedAt && (
<span className="ml-4">
Reviewed: {new Date(request.reviewedAt).toLocaleDateString()}
</span>
)} )}
<div className="text-xs text-gray-500">
Submitted: {new Date(request.createdAt).toLocaleDateString()}
{request.reviewedAt && (
<span className="ml-4">
Reviewed: {new Date(request.reviewedAt).toLocaleDateString()}
</span>
)}
</div>
</div> </div>
</div> ))}
))} </div>
</div> )}
)} </div>
</div> </div>
</div> </div>
</main> </main>

View file

@ -2,7 +2,7 @@
import { Nav } from "@/components/core/nav" import { Nav } from "@/components/core/nav"
import { SiForgejo, SiJellyfin, SiOllama, SiVaultwarden } from "react-icons/si" import { SiForgejo, SiJellyfin, SiOllama, SiVaultwarden } from "react-icons/si"
import { TbMail, TbServer, TbTool, TbKey, TbLogin, TbSend, TbExternalLink, TbInfoCircle } from "react-icons/tb" import { TbMail, TbServer, TbTool, TbKey, TbSend, TbExternalLink, TbInfoCircle } from "react-icons/tb"
import Link from "next/link" import Link from "next/link"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { authClient } from "@/util/auth-client" import { authClient } from "@/util/auth-client"
@ -155,8 +155,8 @@ export default function Services() {
return ( return (
<Link href="/login"> <Link href="/login">
<button className="flex flex-row items-center justify-center gap-1 text-white bg-blue-600 px-3 py-1.5 rounded-lg text-sm hover:bg-blue-700 transition-all duration-300 cursor-pointer"> <button className="flex flex-row items-center justify-center gap-1 text-white bg-blue-600 px-3 py-1.5 rounded-lg text-sm hover:bg-blue-700 transition-all duration-300 cursor-pointer">
<TbLogin size={14} /> <TbSend size={14} />
Login Request
</button> </button>
</Link> </Link>
); );
@ -231,30 +231,33 @@ export default function Services() {
<div className="animate-pulse text-lg">Loading services...</div> <div className="animate-pulse text-lg">Loading services...</div>
</div> </div>
) : ( ) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 my-4 w-full max-w-6xl mx-auto px-4"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 gap-y-14 sm:gap-y-6 my-8 w-full max-w-6xl mx-auto px-4">
{services.map((service) => { {services.map((service) => {
const IconComponent = getServiceIcon(service.name); const IconComponent = getServiceIcon(service.name);
return ( return (
<div key={service.id} className="flex flex-col gap-4"> <div key={service.id} className="group">
<div className={`flex flex-col gap-4 text-base sm:text-lg px-6 sm:px-8 py-6 sm:py-8 rounded-2xl sm:rounded-4xl transition-all ${getServiceCardColor(service)}`}> <Link href={`/services/${service.name}`} className="block">
<Link href={`/services/${service.name}`} className="hover:opacity-90 transition-opacity"> <div className={`flex flex-col gap-4 text-base sm:text-lg px-6 sm:px-8 py-6 sm:py-8 rounded-2xl transition-all duration-300 transform group-hover:scale-105 group-hover:shadow-xl cursor-pointer ${getServiceCardColor(service)} shadow-lg hover:shadow-2xl`}>
<div className="flex flex-row items-center gap-3"> <div className="flex flex-row items-center gap-3">
<IconComponent size={28} className="sm:w-9 sm:h-9" /> <IconComponent size={32} className="sm:w-10 sm:h-10" />
<span className="text-xl sm:text-2xl font-bold"> <span className="text-xl sm:text-2xl font-bold">
{service.name === 'mail' ? 'email' : service.name} {service.name === 'mail' ? 'email' : service.name}
</span> </span>
</div> </div>
</Link> <p className="text-sm sm:text-base opacity-90 line-clamp-2">
<div className="flex flex-row mt-2 gap-3"> {service.description}
{getServiceButtonContent(service)} </p>
<Link href={`/services/${service.name}`} target="_blank" rel="noopener noreferrer">
<button className="flex flex-row items-center justify-center gap-1 text-white bg-green-600 px-3 py-1.5 rounded-lg text-sm hover:bg-green-700 transition-all duration-300 cursor-pointer">
<TbInfoCircle size={14} />
Info
</button>
</Link>
</div> </div>
</Link>
<div className="flex flex-row mt-4 gap-3 justify-center">
{getServiceButtonContent(service)}
<Link href={`/services/${service.name}`}>
<button className="flex flex-row items-center justify-center gap-1 text-white bg-gray-600 px-3 py-1.5 rounded-lg text-sm hover:bg-gray-700 transition-all duration-300 cursor-pointer">
<TbInfoCircle size={14} />
Info
</button>
</Link>
</div> </div>
</div> </div>
); );

View file

@ -3,6 +3,8 @@
import Link from "next/link"; import Link from "next/link";
import { authClient } from "@/util/auth-client"; import { authClient } from "@/util/auth-client";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useState } from "react";
import { TbMenu2, TbX } from "react-icons/tb";
interface ExtendedUser { interface ExtendedUser {
id: string; id: string;
@ -18,6 +20,7 @@ interface ExtendedUser {
export function Nav() { export function Nav() {
const { data: session, isPending } = authClient.useSession(); const { data: session, isPending } = authClient.useSession();
const router = useRouter(); const router = useRouter();
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const handleSignOut = async () => { const handleSignOut = async () => {
try { try {
@ -32,43 +35,162 @@ export function Nav() {
}; };
return ( return (
<div className="flex flex-col sm:flex-row items-center justify-between px-4 sm:px-5 py-3 gap-3 sm:gap-0"> <nav className="relative">
<Link href="/"> <div className="flex items-center justify-between px-4 sm:px-5 py-3">
<h1 className="text-2xl sm:text-3xl font-bold font-mono"> <Link href="/">
p0ntus <h1 className="text-2xl sm:text-3xl font-bold font-mono">
</h1> p0ntus
</Link> </h1>
<div className="flex flex-row flex-wrap items-center justify-center gap-3 sm:gap-4 text-sm sm:text-base"> </Link>
<Link href="/" className="hover:underline">Home</Link>
<Link href="/about" className="hover:underline">About</Link> <button
<Link href="/servers" className="hover:underline">Servers</Link> className="md:hidden p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
<Link href="/services" className="hover:underline">Services</Link> onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
{isPending ? ( aria-label="Toggle menu"
<div className="text-gray-500">Loading...</div> >
) : session ? ( {isMobileMenuOpen ? <TbX size={24} /> : <TbMenu2 size={24} />}
<div className="flex items-center gap-3"> </button>
<Link href="/dashboard" className="hover:underline">Dashboard</Link>
<Link href="/requests" className="hover:underline">Requests</Link> <div className="hidden md:flex flex-row items-center gap-4 text-sm lg:text-base">
{(session.user as ExtendedUser).role === 'admin' && ( <Link href="/" className="hover:underline transition-colors">Home</Link>
<Link href="/admin" className="hover:underline text-red-500">Admin</Link> <Link href="/about" className="hover:underline transition-colors">About</Link>
)} <Link href="/servers" className="hover:underline transition-colors">Servers</Link>
<span className="text-foreground-muted-light ml-6">Hi, <span className="font-bold text-foreground">{session.user.name || session.user.email}</span></span> <Link href="/services" className="hover:underline transition-colors">Services</Link>
<button {isPending ? (
onClick={handleSignOut} <div className="text-gray-500 dark:text-gray-400 animate-pulse">Loading...</div>
className="text-red-400 hover:underline cursor-pointer" ) : session ? (
> <div className="flex items-center gap-3 lg:gap-4">
Sign Out <Link href="/dashboard" className="hover:underline transition-colors">Dashboard</Link>
</button> <Link href="/requests" className="hover:underline transition-colors">Requests</Link>
</div> {(session.user as ExtendedUser).role === 'admin' && (
) : ( <Link href="/admin" className="hover:underline text-red-500 transition-colors">Admin</Link>
<div className="flex items-center gap-3"> )}
<Link href="/login" className="hover:underline">Login</Link> <div className="hidden lg:flex items-center gap-3 ml-4">
<Link href="/signup" className="bg-blue-400 text-white px-3 py-1 rounded-md hover:bg-blue-500"> <span className="text-gray-600 dark:text-gray-300">Hi, <span className="font-bold text-gray-900 dark:text-gray-100">{session.user.name || session.user.email}</span></span>
Sign Up <button
</Link> onClick={handleSignOut}
</div> className="text-red-400 dark:text-red-400 hover:text-red-600 dark:hover:text-red-300 hover:underline cursor-pointer transition-colors"
)} >
Sign Out
</button>
</div>
<div className="lg:hidden">
<button
onClick={handleSignOut}
className="text-red-400 dark:text-red-400 hover:text-red-600 dark:hover:text-red-300 hover:underline cursor-pointer transition-colors text-sm"
>
Sign Out
</button>
</div>
</div>
) : (
<div className="flex items-center gap-3">
<Link href="/login" className="hover:underline transition-colors">Login</Link>
<Link href="/signup" className="bg-blue-500 dark:bg-blue-600 text-white px-3 py-1.5 rounded-md hover:bg-blue-600 dark:hover:bg-blue-700 transition-colors">
Sign Up
</Link>
</div>
)}
</div>
</div> </div>
</div>
<div className={`md:hidden transition-all duration-300 ease-in-out overflow-hidden ${
isMobileMenuOpen ? 'max-h-96 opacity-100' : 'max-h-0 opacity-0'
}`}>
<div className="px-4 py-3 bg-gray-50 dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700">
<div className="flex flex-col gap-3">
<Link
href="/"
className="hover:underline transition-colors py-1"
onClick={() => setIsMobileMenuOpen(false)}
>
Home
</Link>
<Link
href="/about"
className="hover:underline transition-colors py-1"
onClick={() => setIsMobileMenuOpen(false)}
>
About
</Link>
<Link
href="/servers"
className="hover:underline transition-colors py-1"
onClick={() => setIsMobileMenuOpen(false)}
>
Servers
</Link>
<Link
href="/services"
className="hover:underline transition-colors py-1"
onClick={() => setIsMobileMenuOpen(false)}
>
Services
</Link>
{isPending ? (
<div className="text-gray-500 dark:text-gray-400 animate-pulse py-1">Loading...</div>
) : session ? (
<div className="flex flex-col gap-3 pt-2 border-t border-gray-200 dark:border-gray-700">
<Link
href="/dashboard"
className="hover:underline transition-colors py-1"
onClick={() => setIsMobileMenuOpen(false)}
>
Dashboard
</Link>
<Link
href="/requests"
className="hover:underline transition-colors py-1"
onClick={() => setIsMobileMenuOpen(false)}
>
Requests
</Link>
{(session.user as ExtendedUser).role === 'admin' && (
<Link
href="/admin"
className="hover:underline text-red-500 transition-colors py-1"
onClick={() => setIsMobileMenuOpen(false)}
>
Admin
</Link>
)}
<div className="py-2 border-t border-gray-200 dark:border-gray-700">
<span className="text-gray-600 dark:text-gray-300 text-sm block mb-2">
Hi, <span className="font-bold text-gray-900 dark:text-gray-100">{session.user.name || session.user.email}</span>
</span>
<button
onClick={() => {
handleSignOut();
setIsMobileMenuOpen(false);
}}
className="text-red-400 dark:text-red-400 hover:text-red-600 dark:hover:text-red-300 hover:underline cursor-pointer transition-colors text-sm"
>
Sign Out
</button>
</div>
</div>
) : (
<div className="flex flex-col gap-3 pt-2 border-t border-gray-200 dark:border-gray-700">
<Link
href="/login"
className="hover:underline transition-colors py-1"
onClick={() => setIsMobileMenuOpen(false)}
>
Login
</Link>
<Link
href="/signup"
className="bg-blue-500 dark:bg-blue-600 text-white px-3 py-2 rounded-md hover:bg-blue-600 dark:hover:bg-blue-700 transition-colors text-center"
onClick={() => setIsMobileMenuOpen(false)}
>
Sign Up
</Link>
</div>
)}
</div>
</div>
</div>
</nav>
); );
} }

View file

@ -6,7 +6,6 @@ import {
} from "react-icons/si" } from "react-icons/si"
import { import {
TbBrowser, TbBrowser,
TbBubbleText,
TbDeviceTv, TbDeviceTv,
TbGitBranch, TbGitBranch,
TbKey, TbKey,
@ -101,10 +100,6 @@ export const services = [
icon: SiOllama, icon: SiOllama,
priceStatus: "invite-only", priceStatus: "invite-only",
adminView: { adminView: {
"Your chats": {
icon: TbBubbleText,
description: "Your chats are visible to admins.",
},
"Your email address": { "Your email address": {
icon: TbMail, icon: TbMail,
description: "Your email address is visible to admins.", description: "Your email address is visible to admins.",
@ -200,6 +195,7 @@ export const services = [
description: "A private password manager. Powered by Vaultwarden.", description: "A private password manager. Powered by Vaultwarden.",
icon: SiVaultwarden, icon: SiVaultwarden,
priceStatus: "open", priceStatus: "open",
joinLink: "https://pass.librecloud.cc",
adminView: { adminView: {
"Your total entry count": { "Your total entry count": {
icon: TbServer, icon: TbServer,

View file

@ -11,27 +11,27 @@
}, },
"dependencies": { "dependencies": {
"@types/react-world-flags": "^1.6.0", "@types/react-world-flags": "^1.6.0",
"altcha": "^2.0.5", "altcha": "^2.1.0",
"altcha-lib": "^1.3.0", "altcha-lib": "^1.3.0",
"better-auth": "^1.2.12", "better-auth": "^1.3.3",
"drizzle-orm": "^0.44.2", "drizzle-orm": "^0.44.3",
"nanoid": "^5.0.0", "nanoid": "^5.1.5",
"next": "15.3.4", "next": "15.3.4",
"postgres": "^3.4.7", "postgres": "^3.4.7",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-hook-form": "^7.60.0", "react-hook-form": "^7.61.1",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-world-flags": "^1.6.0" "react-world-flags": "^1.6.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",
"@tailwindcss/postcss": "^4.1.11", "@tailwindcss/postcss": "^4.1.11",
"@types/node": "^20.19.4", "@types/node": "^20.19.9",
"@types/react": "^19.1.8", "@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6", "@types/react-dom": "^19.1.6",
"drizzle-kit": "^0.31.4", "drizzle-kit": "^0.31.4",
"eslint": "^9.30.1", "eslint": "^9.31.0",
"eslint-config-next": "15.3.4", "eslint-config-next": "15.3.4",
"tailwindcss": "^4.1.11", "tailwindcss": "^4.1.11",
"tsx": "^4.20.3", "tsx": "^4.20.3",

View file

@ -19,6 +19,7 @@ async function seedDatabase() {
console.log(`✓ Added service: ${service.name}`); console.log(`✓ Added service: ${service.name}`);
} }
console.log("Database seeded!"); console.log("Database seeded!");
process.exit(0);
} catch (error) { } catch (error) {
console.error("Error seeding database:", error); console.error("Error seeding database:", error);
process.exit(1); process.exit(1);