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 className="flex flex-col sm:flex-row items-center justify-center gap-4 my-3">
<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} />
<span className="text-sm sm:text-base">
contact
@ -75,7 +75,7 @@ export default function About() {
</button>
</Link>
<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} />
<span className="text-sm sm:text-base">
join channel

View file

@ -11,7 +11,7 @@ async function getChallenge() {
const challenge = await createChallenge({
hmacKey,
maxNumber: 1400000,
maxNumber: 1000000,
})
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>
<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 ? (
<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) => {
const IconComponent = getServiceIcon(service.name);
return (
<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" />
<h3 className="text-base sm:text-lg font-bold">{service.name}</h3>
</Link>
@ -96,14 +103,14 @@ export default function Home() {
</div>
<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>
<div className="flex flex-col items-center gap-6 mt-6">
<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 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>
<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} />
@ -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>
<div className="flex flex-col items-center gap-6 mt-6">
<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 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>
</div>
</div>

View file

@ -199,155 +199,157 @@ export default function ServiceRequests() {
return (
<main>
<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">
<TbSend size={32} className="text-blue-500" />
<h1 className="text-3xl sm:text-4xl font-bold">Service Requests</h1>
</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">
<h2 className="text-xl font-semibold mb-4">Request Service Access</h2>
<div className="bg-blue-50 dark:bg-blue-900/20 p-4 rounded-lg border border-blue-200 dark:border-blue-700 mb-6">
<div className="flex items-start gap-3">
<TbInfoCircle className="w-5 h-5 text-blue-500 mt-0.5 flex-shrink-0" />
<div>
<p className="text-sm text-blue-700 dark:text-blue-300">
<strong>Note:</strong> Open services (like Git and Mail) don&apos;t require requests - you already have access!
Visit the <Link href="/dashboard" className="underline hover:no-underline">dashboard</Link> to see your available services
and create accounts directly.
</p>
<div className="grid grid-cols-1 md:grid-cols-2 md:gap-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">
<h2 className="text-xl font-semibold mb-4">Request Service Access</h2>
<div className="bg-blue-50 dark:bg-blue-900/20 p-4 rounded-lg border border-blue-200 dark:border-blue-700 mb-6">
<div className="flex items-start gap-3">
<TbInfoCircle className="w-5 h-5 text-blue-500 mt-0.5 flex-shrink-0" />
<div>
<p className="text-sm text-blue-700 dark:text-blue-300">
<strong>Note:</strong> Open services (like Git and Mail) don&apos;t require requests - you already have access!
Visit the <Link href="/dashboard" className="underline hover:no-underline">dashboard</Link> to see your available services
and create accounts directly.
</p>
</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>
<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 className="bg-white dark:bg-gray-800 p-6 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 mb-8">
<h2 className="text-xl font-semibold mb-4 flex items-center gap-2">
<TbEye className="w-5 h-5" />
My Requests
</h2>
<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 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>
{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 className={`flex items-center gap-2 px-3 py-1 rounded-full border ${getStatusColor(request.status)}`}>
{getStatusIcon(request.status)}
<span className="text-sm font-medium capitalize">{request.status}</span>
</div>
</div>
<div className={`flex items-center gap-2 px-3 py-1 rounded-full border ${getStatusColor(request.status)}`}>
{getStatusIcon(request.status)}
<span className="text-sm font-medium capitalize">{request.status}</span>
<div className="mb-3">
<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 className="mb-3">
<p className="text-sm text-gray-600 dark:text-gray-400 mb-1">Reason:</p>
<p className="text-sm">{request.reason}</p>
</div>
{request.adminNotes && (
<div className="mb-3 p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
<p className="text-sm text-gray-600 dark:text-gray-400 mb-1 flex items-center gap-1">
<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>
{request.adminNotes && (
<div className="mb-3 p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
<p className="text-sm text-gray-600 dark:text-gray-400 mb-1 flex items-center gap-1">
<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>
</div>
</div>
))}
</div>
)}
))}
</div>
)}
</div>
</div>
</div>
</main>

View file

@ -2,7 +2,7 @@
import { Nav } from "@/components/core/nav"
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 { useEffect, useState } from "react"
import { authClient } from "@/util/auth-client"
@ -155,8 +155,8 @@ export default function Services() {
return (
<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">
<TbLogin size={14} />
Login
<TbSend size={14} />
Request
</button>
</Link>
);
@ -231,30 +231,33 @@ export default function Services() {
<div className="animate-pulse text-lg">Loading services...</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) => {
const IconComponent = getServiceIcon(service.name);
return (
<div key={service.id} className="flex flex-col gap-4">
<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="hover:opacity-90 transition-opacity">
<div key={service.id} className="group">
<Link href={`/services/${service.name}`} className="block">
<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">
<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">
{service.name === 'mail' ? 'email' : service.name}
</span>
</div>
</Link>
<div className="flex flex-row mt-2 gap-3">
{getServiceButtonContent(service)}
<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>
<p className="text-sm sm:text-base opacity-90 line-clamp-2">
{service.description}
</p>
</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>
);

View file

@ -3,6 +3,8 @@
import Link from "next/link";
import { authClient } from "@/util/auth-client";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { TbMenu2, TbX } from "react-icons/tb";
interface ExtendedUser {
id: string;
@ -18,6 +20,7 @@ interface ExtendedUser {
export function Nav() {
const { data: session, isPending } = authClient.useSession();
const router = useRouter();
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const handleSignOut = async () => {
try {
@ -32,43 +35,162 @@ export function Nav() {
};
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">
<Link href="/">
<h1 className="text-2xl sm:text-3xl font-bold font-mono">
p0ntus
</h1>
</Link>
<div className="flex flex-row flex-wrap items-center justify-center gap-3 sm:gap-4 text-sm sm:text-base">
<Link href="/" className="hover:underline">Home</Link>
<Link href="/about" className="hover:underline">About</Link>
<Link href="/servers" className="hover:underline">Servers</Link>
<Link href="/services" className="hover:underline">Services</Link>
{isPending ? (
<div className="text-gray-500">Loading...</div>
) : session ? (
<div className="flex items-center gap-3">
<Link href="/dashboard" className="hover:underline">Dashboard</Link>
<Link href="/requests" className="hover:underline">Requests</Link>
{(session.user as ExtendedUser).role === 'admin' && (
<Link href="/admin" className="hover:underline text-red-500">Admin</Link>
)}
<span className="text-foreground-muted-light ml-6">Hi, <span className="font-bold text-foreground">{session.user.name || session.user.email}</span></span>
<button
onClick={handleSignOut}
className="text-red-400 hover:underline cursor-pointer"
>
Sign Out
</button>
</div>
) : (
<div className="flex items-center gap-3">
<Link href="/login" className="hover:underline">Login</Link>
<Link href="/signup" className="bg-blue-400 text-white px-3 py-1 rounded-md hover:bg-blue-500">
Sign Up
</Link>
</div>
)}
<nav className="relative">
<div className="flex items-center justify-between px-4 sm:px-5 py-3">
<Link href="/">
<h1 className="text-2xl sm:text-3xl font-bold font-mono">
p0ntus
</h1>
</Link>
<button
className="md:hidden p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
aria-label="Toggle menu"
>
{isMobileMenuOpen ? <TbX size={24} /> : <TbMenu2 size={24} />}
</button>
<div className="hidden md:flex flex-row items-center gap-4 text-sm lg:text-base">
<Link href="/" className="hover:underline transition-colors">Home</Link>
<Link href="/about" className="hover:underline transition-colors">About</Link>
<Link href="/servers" className="hover:underline transition-colors">Servers</Link>
<Link href="/services" className="hover:underline transition-colors">Services</Link>
{isPending ? (
<div className="text-gray-500 dark:text-gray-400 animate-pulse">Loading...</div>
) : session ? (
<div className="flex items-center gap-3 lg:gap-4">
<Link href="/dashboard" className="hover:underline transition-colors">Dashboard</Link>
<Link href="/requests" className="hover:underline transition-colors">Requests</Link>
{(session.user as ExtendedUser).role === 'admin' && (
<Link href="/admin" className="hover:underline text-red-500 transition-colors">Admin</Link>
)}
<div className="hidden lg:flex items-center gap-3 ml-4">
<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>
<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"
>
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 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"
import {
TbBrowser,
TbBubbleText,
TbDeviceTv,
TbGitBranch,
TbKey,
@ -101,10 +100,6 @@ export const services = [
icon: SiOllama,
priceStatus: "invite-only",
adminView: {
"Your chats": {
icon: TbBubbleText,
description: "Your chats are visible to admins.",
},
"Your email address": {
icon: TbMail,
description: "Your email address is visible to admins.",
@ -200,6 +195,7 @@ export const services = [
description: "A private password manager. Powered by Vaultwarden.",
icon: SiVaultwarden,
priceStatus: "open",
joinLink: "https://pass.librecloud.cc",
adminView: {
"Your total entry count": {
icon: TbServer,

View file

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

View file

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