From 3f8fe82bfe9c3064ae39c7102c4b304ebba46a1a Mon Sep 17 00:00:00 2001 From: Kevin Reber Date: Sun, 17 Nov 2024 08:25:05 -0800 Subject: [PATCH] Added settings route --- @/components/ui/alert.tsx | 59 ++++++++++++++ app/routes/settings.tsx | 139 +++++++++++++++++++++++++++++++++ app/services/stripe.server.ts | 2 +- app/services/webhook.server.ts | 5 +- 4 files changed, 202 insertions(+), 3 deletions(-) create mode 100644 @/components/ui/alert.tsx create mode 100644 app/routes/settings.tsx diff --git a/@/components/ui/alert.tsx b/@/components/ui/alert.tsx new file mode 100644 index 0000000..41fa7e0 --- /dev/null +++ b/@/components/ui/alert.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)) +Alert.displayName = "Alert" + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertTitle.displayName = "AlertTitle" + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertDescription.displayName = "AlertDescription" + +export { Alert, AlertTitle, AlertDescription } diff --git a/app/routes/settings.tsx b/app/routes/settings.tsx new file mode 100644 index 0000000..002e3be --- /dev/null +++ b/app/routes/settings.tsx @@ -0,0 +1,139 @@ +import { json, type ActionFunctionArgs } from "@remix-run/node"; +import { Form, useActionData } from "@remix-run/react"; +import { prisma } from "~/services/prisma.server"; +import { requireUserLogin } from "~/services/auth.server"; +import { z } from "zod"; +import { PageContainer } from "~/components"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { useLoggedInUser } from "~/hooks"; + +const UsernameSchema = z + .string() + .min(3, "Username must be at least 3 characters long") + .max(30, "Username cannot exceed 30 characters") + .regex( + /^[a-zA-Z0-9_-]+$/, + "Username can only contain letters, numbers, underscores, and hyphens" + ) + .transform((username) => username.toLowerCase()); + +export async function loader({ request }: ActionFunctionArgs) { + const user = await requireUserLogin(request); + return json({ user }); +} + +export async function action({ request }: ActionFunctionArgs) { + const user = await requireUserLogin(request); + if (!user) { + throw new Response("Unauthorized", { status: 401 }); + } + + const formData = await request.formData(); + const rawUsername = formData.get("username"); + + const usernameResult = UsernameSchema.safeParse(rawUsername); + + if (!usernameResult.success) { + return json( + { error: usernameResult.error.errors[0].message }, + { status: 400 } + ); + } + + const username = usernameResult.data; + + try { + const existingUser = await prisma.user.findFirst({ + where: { + username, + NOT: { + id: user.id, + }, + }, + }); + + if (existingUser) { + return json( + { error: "This username is already taken. Please choose another one." }, + { status: 400 } + ); + } + + const updatedUser = await prisma.user.update({ + where: { id: user.id }, + data: { username }, + }); + + return json({ success: true, user: updatedUser }); + } catch (error) { + console.error("Error updating username:", error); + return json( + { error: "Failed to update username. Please try again." }, + { status: 500 } + ); + } +} + +export default function SettingsPage() { + const user = useLoggedInUser(); + const actionData = useActionData(); + + return ( + +
+ + + + Account Settings + + + +
+
+ + +

+ Username can only contain letters, numbers, underscores, and + hyphens. +

+
+ + {actionData?.error && ( +
+ {actionData.error} +
+ )} + + {actionData?.success && ( +
+ Username updated successfully! +
+ )} + + +
+
+
+
+
+ ); +} diff --git a/app/services/stripe.server.ts b/app/services/stripe.server.ts index b1e4e5a..3ebab37 100644 --- a/app/services/stripe.server.ts +++ b/app/services/stripe.server.ts @@ -19,7 +19,7 @@ export const stripeCheckout = async ({ userId }: { userId: string }) => { }); return session.url!; - } catch (error: any) { + } catch (error: Error) { console.error(error); throw json({ errors: [{ message: error.message }] }, 400); } diff --git a/app/services/webhook.server.ts b/app/services/webhook.server.ts index dcf9c6f..67b4937 100644 --- a/app/services/webhook.server.ts +++ b/app/services/webhook.server.ts @@ -18,7 +18,7 @@ export const handleStripeEvent = async ( } switch (type) { - case CHECKOUT_SESSION_COMPLETED: + case CHECKOUT_SESSION_COMPLETED: { const checkoutSessionCompleted = data.object as { id: string; amount: number; @@ -29,7 +29,7 @@ export const handleStripeEvent = async ( const creditsToAdd = 100; console.log("CHECKOUT SESSION COMPLETED: ", checkoutSessionCompleted); - // Update users credits in DB after checkout + const userData = await prisma.user.update({ where: { id: checkoutSessionCompleted.metadata.userId, @@ -42,6 +42,7 @@ export const handleStripeEvent = async ( }); console.log("DONE UPDATING USER DATA: ", userData); return userData; + } default: console.log(`Unhandled event type: ${type}`);