Initial commit
This commit is contained in:
218
actions/payment-methods.ts
Normal file
218
actions/payment-methods.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
"use server";
|
||||
|
||||
import { PaymentMethodType } from "@prisma/client";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { db } from "@/lib/db";
|
||||
import { requireCurrentUser } from "@/lib/session";
|
||||
import { sanitizeOptionalPlainText, sanitizePlainText } from "@/lib/sanitize";
|
||||
import { paymentMethodSchema } from "@/lib/validators/payment-method";
|
||||
|
||||
export type ActionResult = {
|
||||
success: boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
function parseCheckboxValue(value: FormDataEntryValue | null) {
|
||||
return value === "on" || value === "true";
|
||||
}
|
||||
|
||||
async function getOwnedProfile(userId: string) {
|
||||
return db.profile.findUnique({
|
||||
where: { userId },
|
||||
include: {
|
||||
paymentMethods: {
|
||||
orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }]
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function resequencePaymentMethods(profileId: string) {
|
||||
const methods = await db.paymentMethod.findMany({
|
||||
where: { profileId },
|
||||
orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }]
|
||||
});
|
||||
|
||||
await db.$transaction(
|
||||
methods.map((method, index) =>
|
||||
db.paymentMethod.update({
|
||||
where: { id: method.id },
|
||||
data: { sortOrder: index }
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function parsePaymentInput(formData: FormData) {
|
||||
return paymentMethodSchema.safeParse({
|
||||
type: sanitizePlainText(String(formData.get("type") ?? "")) as PaymentMethodType,
|
||||
label: sanitizePlainText(String(formData.get("label") ?? "")),
|
||||
value: sanitizePlainText(String(formData.get("value") ?? "")),
|
||||
network: sanitizeOptionalPlainText(String(formData.get("network") ?? "")) ?? "",
|
||||
description: sanitizeOptionalPlainText(String(formData.get("description") ?? "")) ?? "",
|
||||
isVisible: parseCheckboxValue(formData.get("isVisible"))
|
||||
});
|
||||
}
|
||||
|
||||
export async function createPaymentMethodAction(formData: FormData): Promise<ActionResult> {
|
||||
const user = await requireCurrentUser();
|
||||
const profile = await getOwnedProfile(user.id);
|
||||
|
||||
if (!profile) {
|
||||
return { success: false, message: "Profile not found" };
|
||||
}
|
||||
|
||||
const parsed = parsePaymentInput(formData);
|
||||
if (!parsed.success) {
|
||||
return { success: false, message: parsed.error.issues[0]?.message ?? "Invalid payment method" };
|
||||
}
|
||||
|
||||
const maxSortOrder = profile.paymentMethods.length;
|
||||
|
||||
await db.paymentMethod.create({
|
||||
data: {
|
||||
profileId: profile.id,
|
||||
type: parsed.data.type,
|
||||
label: parsed.data.label,
|
||||
value: parsed.data.value,
|
||||
network: parsed.data.network || null,
|
||||
description: parsed.data.description || null,
|
||||
isVisible: parsed.data.isVisible,
|
||||
sortOrder: maxSortOrder
|
||||
}
|
||||
});
|
||||
|
||||
revalidatePath("/dashboard/payment-methods");
|
||||
revalidatePath(`/u/${profile.username}`);
|
||||
|
||||
return { success: true, message: "Payment method added" };
|
||||
}
|
||||
|
||||
export async function updatePaymentMethodAction(formData: FormData): Promise<ActionResult> {
|
||||
const user = await requireCurrentUser();
|
||||
const profile = await getOwnedProfile(user.id);
|
||||
|
||||
if (!profile) {
|
||||
return { success: false, message: "Profile not found" };
|
||||
}
|
||||
|
||||
const paymentMethodId = sanitizePlainText(String(formData.get("id") ?? ""));
|
||||
const owned = profile.paymentMethods.find((method) => method.id === paymentMethodId);
|
||||
if (!owned) {
|
||||
return { success: false, message: "Payment method not found" };
|
||||
}
|
||||
|
||||
const parsed = parsePaymentInput(formData);
|
||||
if (!parsed.success) {
|
||||
return { success: false, message: parsed.error.issues[0]?.message ?? "Invalid payment method" };
|
||||
}
|
||||
|
||||
await db.paymentMethod.update({
|
||||
where: { id: paymentMethodId },
|
||||
data: {
|
||||
type: parsed.data.type,
|
||||
label: parsed.data.label,
|
||||
value: parsed.data.value,
|
||||
network: parsed.data.network || null,
|
||||
description: parsed.data.description || null,
|
||||
isVisible: parsed.data.isVisible
|
||||
}
|
||||
});
|
||||
|
||||
revalidatePath("/dashboard/payment-methods");
|
||||
revalidatePath(`/u/${profile.username}`);
|
||||
|
||||
return { success: true, message: "Payment method updated" };
|
||||
}
|
||||
|
||||
export async function deletePaymentMethodAction(formData: FormData): Promise<ActionResult> {
|
||||
const user = await requireCurrentUser();
|
||||
const profile = await getOwnedProfile(user.id);
|
||||
|
||||
if (!profile) {
|
||||
return { success: false, message: "Profile not found" };
|
||||
}
|
||||
|
||||
const paymentMethodId = sanitizePlainText(String(formData.get("id") ?? ""));
|
||||
const owned = profile.paymentMethods.find((method) => method.id === paymentMethodId);
|
||||
|
||||
if (!owned) {
|
||||
return { success: false, message: "Payment method not found" };
|
||||
}
|
||||
|
||||
await db.paymentMethod.delete({ where: { id: paymentMethodId } });
|
||||
await resequencePaymentMethods(profile.id);
|
||||
|
||||
revalidatePath("/dashboard/payment-methods");
|
||||
revalidatePath(`/u/${profile.username}`);
|
||||
|
||||
return { success: true, message: "Payment method deleted" };
|
||||
}
|
||||
|
||||
export async function togglePaymentMethodVisibilityAction(formData: FormData): Promise<ActionResult> {
|
||||
const user = await requireCurrentUser();
|
||||
const profile = await getOwnedProfile(user.id);
|
||||
|
||||
if (!profile) {
|
||||
return { success: false, message: "Profile not found" };
|
||||
}
|
||||
|
||||
const paymentMethodId = sanitizePlainText(String(formData.get("id") ?? ""));
|
||||
const owned = profile.paymentMethods.find((method) => method.id === paymentMethodId);
|
||||
|
||||
if (!owned) {
|
||||
return { success: false, message: "Payment method not found" };
|
||||
}
|
||||
|
||||
await db.paymentMethod.update({
|
||||
where: { id: paymentMethodId },
|
||||
data: { isVisible: !owned.isVisible }
|
||||
});
|
||||
|
||||
revalidatePath("/dashboard/payment-methods");
|
||||
revalidatePath(`/u/${profile.username}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: owned.isVisible ? "Payment method hidden" : "Payment method visible"
|
||||
};
|
||||
}
|
||||
|
||||
export async function movePaymentMethodAction(formData: FormData): Promise<ActionResult> {
|
||||
const user = await requireCurrentUser();
|
||||
const profile = await getOwnedProfile(user.id);
|
||||
|
||||
if (!profile) {
|
||||
return { success: false, message: "Profile not found" };
|
||||
}
|
||||
|
||||
const paymentMethodId = sanitizePlainText(String(formData.get("id") ?? ""));
|
||||
const direction = sanitizePlainText(String(formData.get("direction") ?? ""));
|
||||
const index = profile.paymentMethods.findIndex((method) => method.id === paymentMethodId);
|
||||
|
||||
if (index === -1) {
|
||||
return { success: false, message: "Payment method not found" };
|
||||
}
|
||||
|
||||
if (direction !== "up" && direction !== "down") {
|
||||
return { success: false, message: "Invalid sort direction" };
|
||||
}
|
||||
|
||||
const swapIndex = direction === "up" ? index - 1 : index + 1;
|
||||
if (swapIndex < 0 || swapIndex >= profile.paymentMethods.length) {
|
||||
return { success: false, message: "Cannot move further" };
|
||||
}
|
||||
|
||||
const current = profile.paymentMethods[index];
|
||||
const swapWith = profile.paymentMethods[swapIndex];
|
||||
|
||||
await db.$transaction([
|
||||
db.paymentMethod.update({ where: { id: current.id }, data: { sortOrder: swapWith.sortOrder } }),
|
||||
db.paymentMethod.update({ where: { id: swapWith.id }, data: { sortOrder: current.sortOrder } })
|
||||
]);
|
||||
|
||||
revalidatePath("/dashboard/payment-methods");
|
||||
revalidatePath(`/u/${profile.username}`);
|
||||
|
||||
return { success: true, message: "Order updated" };
|
||||
}
|
||||
82
actions/profile.ts
Normal file
82
actions/profile.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
"use server";
|
||||
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { db } from "@/lib/db";
|
||||
import { DEFAULT_THEME_ID } from "@/lib/constants";
|
||||
import { requireCurrentUser } from "@/lib/session";
|
||||
import { normalizeUsername, safeUrl, sanitizeOptionalPlainText, sanitizePlainText } from "@/lib/sanitize";
|
||||
import { profileSchema } from "@/lib/validators/profile";
|
||||
|
||||
export type ActionResult = {
|
||||
success: boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
function parseCheckboxValue(value: FormDataEntryValue | null) {
|
||||
return value === "on" || value === "true";
|
||||
}
|
||||
|
||||
export async function updateProfileAction(formData: FormData): Promise<ActionResult> {
|
||||
const user = await requireCurrentUser();
|
||||
const currentUsername = user.profile?.username;
|
||||
|
||||
const parsed = profileSchema.safeParse({
|
||||
username: normalizeUsername(String(formData.get("username") ?? "")),
|
||||
displayName: sanitizePlainText(String(formData.get("displayName") ?? "")),
|
||||
bio: sanitizeOptionalPlainText(String(formData.get("bio") ?? "")) ?? "",
|
||||
avatarUrl: safeUrl(String(formData.get("avatarUrl") ?? "")) ?? "",
|
||||
themeId: sanitizePlainText(String(formData.get("themeId") ?? DEFAULT_THEME_ID)),
|
||||
isPublic: parseCheckboxValue(formData.get("isPublic"))
|
||||
});
|
||||
|
||||
if (!parsed.success) {
|
||||
return {
|
||||
success: false,
|
||||
message: parsed.error.issues[0]?.message ?? "Invalid profile input"
|
||||
};
|
||||
}
|
||||
|
||||
const theme = await db.theme.findUnique({ where: { id: parsed.data.themeId } });
|
||||
if (!theme) {
|
||||
return { success: false, message: "Invalid theme selection" };
|
||||
}
|
||||
|
||||
try {
|
||||
await db.profile.upsert({
|
||||
where: { userId: user.id },
|
||||
update: {
|
||||
username: parsed.data.username,
|
||||
displayName: parsed.data.displayName,
|
||||
bio: parsed.data.bio || null,
|
||||
avatarUrl: parsed.data.avatarUrl || null,
|
||||
themeId: parsed.data.themeId,
|
||||
isPublic: parsed.data.isPublic
|
||||
},
|
||||
create: {
|
||||
userId: user.id,
|
||||
username: parsed.data.username,
|
||||
displayName: parsed.data.displayName,
|
||||
bio: parsed.data.bio || null,
|
||||
avatarUrl: parsed.data.avatarUrl || null,
|
||||
themeId: parsed.data.themeId,
|
||||
isPublic: parsed.data.isPublic
|
||||
}
|
||||
});
|
||||
|
||||
if (currentUsername) {
|
||||
revalidatePath(`/u/${currentUsername}`);
|
||||
}
|
||||
revalidatePath(`/u/${parsed.data.username}`);
|
||||
revalidatePath("/dashboard");
|
||||
revalidatePath("/dashboard/profile");
|
||||
|
||||
return { success: true, message: "Profile saved" };
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") {
|
||||
return { success: false, message: "Username is already taken" };
|
||||
}
|
||||
|
||||
return { success: false, message: "Could not update profile" };
|
||||
}
|
||||
}
|
||||
164
actions/social-links.ts
Normal file
164
actions/social-links.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { db } from "@/lib/db";
|
||||
import { requireCurrentUser } from "@/lib/session";
|
||||
import { sanitizePlainText } from "@/lib/sanitize";
|
||||
import { socialLinkSchema } from "@/lib/validators/social-link";
|
||||
|
||||
export type ActionResult = {
|
||||
success: boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
function parseCheckboxValue(value: FormDataEntryValue | null) {
|
||||
return value === "on" || value === "true";
|
||||
}
|
||||
|
||||
async function getOwnedProfile(userId: string) {
|
||||
return db.profile.findUnique({
|
||||
where: { userId },
|
||||
include: {
|
||||
socialLinks: {
|
||||
orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }]
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function createSocialLinkAction(formData: FormData): Promise<ActionResult> {
|
||||
const user = await requireCurrentUser();
|
||||
const profile = await getOwnedProfile(user.id);
|
||||
|
||||
if (!profile) {
|
||||
return { success: false, message: "Profile not found" };
|
||||
}
|
||||
|
||||
const parsed = socialLinkSchema.safeParse({
|
||||
label: sanitizePlainText(String(formData.get("label") ?? "")),
|
||||
url: sanitizePlainText(String(formData.get("url") ?? "")),
|
||||
isVisible: parseCheckboxValue(formData.get("isVisible"))
|
||||
});
|
||||
|
||||
if (!parsed.success) {
|
||||
return { success: false, message: parsed.error.issues[0]?.message ?? "Invalid social link" };
|
||||
}
|
||||
|
||||
await db.socialLink.create({
|
||||
data: {
|
||||
profileId: profile.id,
|
||||
label: parsed.data.label,
|
||||
url: parsed.data.url,
|
||||
isVisible: parsed.data.isVisible,
|
||||
sortOrder: profile.socialLinks.length
|
||||
}
|
||||
});
|
||||
|
||||
revalidatePath("/dashboard/social-links");
|
||||
revalidatePath(`/u/${profile.username}`);
|
||||
|
||||
return { success: true, message: "Social link added" };
|
||||
}
|
||||
|
||||
export async function deleteSocialLinkAction(formData: FormData): Promise<ActionResult> {
|
||||
const user = await requireCurrentUser();
|
||||
const profile = await getOwnedProfile(user.id);
|
||||
|
||||
if (!profile) {
|
||||
return { success: false, message: "Profile not found" };
|
||||
}
|
||||
|
||||
const socialLinkId = sanitizePlainText(String(formData.get("id") ?? ""));
|
||||
const owned = profile.socialLinks.find((link) => link.id === socialLinkId);
|
||||
if (!owned) {
|
||||
return { success: false, message: "Social link not found" };
|
||||
}
|
||||
|
||||
await db.socialLink.delete({ where: { id: socialLinkId } });
|
||||
|
||||
const remaining = await db.socialLink.findMany({
|
||||
where: { profileId: profile.id },
|
||||
orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }]
|
||||
});
|
||||
|
||||
await db.$transaction(
|
||||
remaining.map((link, index) =>
|
||||
db.socialLink.update({
|
||||
where: { id: link.id },
|
||||
data: { sortOrder: index }
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
revalidatePath("/dashboard/social-links");
|
||||
revalidatePath(`/u/${profile.username}`);
|
||||
|
||||
return { success: true, message: "Social link deleted" };
|
||||
}
|
||||
|
||||
export async function toggleSocialLinkAction(formData: FormData): Promise<ActionResult> {
|
||||
const user = await requireCurrentUser();
|
||||
const profile = await getOwnedProfile(user.id);
|
||||
|
||||
if (!profile) {
|
||||
return { success: false, message: "Profile not found" };
|
||||
}
|
||||
|
||||
const socialLinkId = sanitizePlainText(String(formData.get("id") ?? ""));
|
||||
const owned = profile.socialLinks.find((link) => link.id === socialLinkId);
|
||||
if (!owned) {
|
||||
return { success: false, message: "Social link not found" };
|
||||
}
|
||||
|
||||
await db.socialLink.update({
|
||||
where: { id: socialLinkId },
|
||||
data: { isVisible: !owned.isVisible }
|
||||
});
|
||||
|
||||
revalidatePath("/dashboard/social-links");
|
||||
revalidatePath(`/u/${profile.username}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: owned.isVisible ? "Social link hidden" : "Social link visible"
|
||||
};
|
||||
}
|
||||
|
||||
export async function moveSocialLinkAction(formData: FormData): Promise<ActionResult> {
|
||||
const user = await requireCurrentUser();
|
||||
const profile = await getOwnedProfile(user.id);
|
||||
|
||||
if (!profile) {
|
||||
return { success: false, message: "Profile not found" };
|
||||
}
|
||||
|
||||
const socialLinkId = sanitizePlainText(String(formData.get("id") ?? ""));
|
||||
const direction = sanitizePlainText(String(formData.get("direction") ?? ""));
|
||||
const index = profile.socialLinks.findIndex((link) => link.id === socialLinkId);
|
||||
|
||||
if (index === -1) {
|
||||
return { success: false, message: "Social link not found" };
|
||||
}
|
||||
|
||||
if (direction !== "up" && direction !== "down") {
|
||||
return { success: false, message: "Invalid sort direction" };
|
||||
}
|
||||
|
||||
const swapIndex = direction === "up" ? index - 1 : index + 1;
|
||||
if (swapIndex < 0 || swapIndex >= profile.socialLinks.length) {
|
||||
return { success: false, message: "Cannot move further" };
|
||||
}
|
||||
|
||||
const current = profile.socialLinks[index];
|
||||
const swapWith = profile.socialLinks[swapIndex];
|
||||
|
||||
await db.$transaction([
|
||||
db.socialLink.update({ where: { id: current.id }, data: { sortOrder: swapWith.sortOrder } }),
|
||||
db.socialLink.update({ where: { id: swapWith.id }, data: { sortOrder: current.sortOrder } })
|
||||
]);
|
||||
|
||||
revalidatePath("/dashboard/social-links");
|
||||
revalidatePath(`/u/${profile.username}`);
|
||||
|
||||
return { success: true, message: "Order updated" };
|
||||
}
|
||||
Reference in New Issue
Block a user