Initial commit

This commit is contained in:
2026-03-27 19:35:14 +01:00
commit 38581b88a4
68 changed files with 12137 additions and 0 deletions

View File

@@ -0,0 +1,33 @@
import Link from "next/link";
import type { ReactNode } from "react";
import { SignOutButton } from "@/components/auth/signout-button";
import { Sidebar } from "@/components/dashboard/sidebar";
import { requireCurrentUser } from "@/lib/session";
export default async function DashboardLayout({
children
}: {
children: ReactNode;
}) {
const user = await requireCurrentUser();
return (
<div className="dashboard-shell flex min-h-screen flex-col">
<header className="mb-7 flex flex-wrap items-center justify-between gap-4 border-b border-border/80 pb-5">
<div className="space-y-1">
<Link href="/" className="terminal-heading">
PayMe
</Link>
<p className="text-sm text-muted">{user.email}</p>
</div>
<SignOutButton />
</header>
<div className="flex flex-col gap-7 md:flex-row">
<Sidebar username={user.profile?.username} />
<main className="min-w-0 flex-1">{children}</main>
</div>
</div>
);
}

View File

@@ -0,0 +1,43 @@
import { db } from "@/lib/db";
import { requireCurrentUser } from "@/lib/session";
export default async function DashboardOverviewPage() {
const user = await requireCurrentUser();
const profile = await db.profile.findUnique({
where: { userId: user.id },
include: {
paymentMethods: true,
socialLinks: true
}
});
return (
<section className="space-y-8">
<div className="space-y-2">
<h1 className="text-2xl font-semibold">Overview</h1>
<p className="text-sm text-muted">
Manage your public payment profile. PayMe only displays payment details and links.
</p>
</div>
<div className="grid gap-4 md:grid-cols-3">
<article className="terminal-card py-5">
<p className="terminal-heading">Payment Methods</p>
<p className="mt-4 text-3xl font-semibold">{profile?.paymentMethods.length ?? 0}</p>
</article>
<article className="terminal-card py-5">
<p className="terminal-heading">Social Links</p>
<p className="mt-4 text-3xl font-semibold">{profile?.socialLinks.length ?? 0}</p>
</article>
<article className="terminal-card py-5">
<p className="terminal-heading">Public Visibility</p>
<p className="mt-4 text-3xl font-semibold">{profile?.isPublic ? "On" : "Off"}</p>
</article>
</div>
</section>
);
}

View File

@@ -0,0 +1,45 @@
import { PaymentMethodsForm } from "@/components/dashboard/payment-methods-form";
import { PaymentMethodsList } from "@/components/dashboard/payment-methods-list";
import { db } from "@/lib/db";
import { requireCurrentUser } from "@/lib/session";
export default async function DashboardPaymentMethodsPage() {
const user = await requireCurrentUser();
const profile = await db.profile.findUnique({
where: { userId: user.id },
include: {
paymentMethods: {
select: {
id: true,
type: true,
label: true,
value: true,
network: true,
description: true,
sortOrder: true,
isVisible: true
},
orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }]
}
}
});
if (!profile) {
return <p className="text-sm text-red-300">Profile not found.</p>;
}
return (
<section className="space-y-6">
<header className="space-y-2">
<h1 className="text-2xl font-semibold">Payment Methods</h1>
<p className="text-sm text-muted">
Add, edit, reorder, and toggle payment methods. Validation is format-based only and not authoritative.
</p>
</header>
<PaymentMethodsForm />
<PaymentMethodsList methods={profile.paymentMethods} />
</section>
);
}

View File

@@ -0,0 +1,37 @@
import { ProfileForm } from "@/components/dashboard/profile-form";
import { db } from "@/lib/db";
import { DEFAULT_THEME_ID } from "@/lib/constants";
import { requireCurrentUser } from "@/lib/session";
export default async function DashboardProfilePage() {
const user = await requireCurrentUser();
const [profile, themes] = await Promise.all([
db.profile.findUnique({
where: { userId: user.id }
}),
db.theme.findMany({ orderBy: { name: "asc" } })
]);
return (
<section className="space-y-6">
<header className="space-y-2">
<h1 className="text-2xl font-semibold">Profile</h1>
<p className="text-sm text-muted">
Update how your public page appears. Username changes update your public URL.
</p>
</header>
<ProfileForm
initialValues={{
username: profile?.username ?? "",
displayName: profile?.displayName ?? "",
bio: profile?.bio ?? "",
avatarUrl: profile?.avatarUrl ?? "",
themeId: profile?.themeId ?? DEFAULT_THEME_ID,
isPublic: profile?.isPublic ?? true
}}
themes={themes}
/>
</section>
);
}

View File

@@ -0,0 +1,40 @@
import { SocialLinksForm } from "@/components/dashboard/social-links-form";
import { db } from "@/lib/db";
import { requireCurrentUser } from "@/lib/session";
export default async function DashboardSocialLinksPage() {
const user = await requireCurrentUser();
const profile = await db.profile.findUnique({
where: { userId: user.id },
include: {
socialLinks: {
select: {
id: true,
label: true,
url: true,
sortOrder: true,
isVisible: true
},
orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }]
}
}
});
if (!profile) {
return <p className="text-sm text-red-300">Profile not found.</p>;
}
return (
<section className="space-y-6">
<header className="space-y-2">
<h1 className="text-2xl font-semibold">Social Links</h1>
<p className="text-sm text-muted">
Optional links to contact points and social profiles. Keep this short and relevant.
</p>
</header>
<SocialLinksForm links={profile.socialLinks} />
</section>
);
}