mirror of
https://github.com/trafficlunar/tomodachi-share.git
synced 2026-06-28 14:44:15 +00:00
feat: ability to update display name and username in profile settings
This commit is contained in:
parent
86c76df873
commit
8b8c9aaa4b
10 changed files with 156 additions and 38 deletions
28
src/app/api/auth/display-name/route.ts
Normal file
28
src/app/api/auth/display-name/route.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { displayNameSchema } from "@/lib/schemas";
|
||||
|
||||
export async function PATCH(request: NextRequest) {
|
||||
const session = await auth();
|
||||
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
|
||||
const { displayName } = await request.json();
|
||||
if (!displayName) return NextResponse.json({ error: "New display name is required" }, { status: 400 });
|
||||
|
||||
const validation = displayNameSchema.safeParse(displayName);
|
||||
if (!validation.success) return NextResponse.json({ error: validation.error.errors[0].message }, { status: 400 });
|
||||
|
||||
try {
|
||||
await prisma.user.update({
|
||||
where: { email: session.user?.email ?? undefined },
|
||||
data: { name: displayName },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to update display name:", error);
|
||||
return NextResponse.json({ error: "Failed to update display name" }, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
|
@ -1,21 +1,15 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
const usernameSchema = z
|
||||
.string()
|
||||
.min(3, "Username must be at least 3 characters long")
|
||||
.max(20, "Username cannot be more than 20 characters long")
|
||||
.regex(/^[a-zA-Z0-9_]+$/, "Username can only contain letters, numbers, and underscores");
|
||||
import { usernameSchema } from "@/lib/schemas";
|
||||
|
||||
export async function PATCH(request: NextRequest) {
|
||||
const session = await auth();
|
||||
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
|
||||
const { username } = await request.json();
|
||||
if (!username) return NextResponse.json({ error: "Username is required" }, { status: 400 });
|
||||
if (!username) return NextResponse.json({ error: "New username is required" }, { status: 400 });
|
||||
|
||||
const validation = usernameSchema.safeParse(username);
|
||||
if (!validation.success) return NextResponse.json({ error: validation.error.errors[0].message }, { status: 400 });
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export default function DeleteMiiButton({ miiId, miiName, likes }: Props) {
|
|||
}
|
||||
|
||||
close();
|
||||
window.location.reload(); // I would use router.refresh() here but the API data fetching breaks
|
||||
window.location.reload(); // I would use router.refresh() here but the Mii list doesn't update
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
|
|
|
|||
|
|
@ -1,23 +1,76 @@
|
|||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { useState } from "react";
|
||||
import SubmitDialogButton from "./submit-dialog-button";
|
||||
import DeleteAccount from "./delete-account";
|
||||
|
||||
interface Props {
|
||||
name: string | null | undefined;
|
||||
username: string | null | undefined;
|
||||
}
|
||||
import { displayNameSchema, usernameSchema } from "@/lib/schemas";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
export default function ProfileSettings({ name, username }: Props) {
|
||||
const [displayName, setDisplayName] = useState(name ?? "");
|
||||
const [usernameState, setUsernameState] = useState(username ?? "");
|
||||
export default function ProfileSettings() {
|
||||
const router = useRouter();
|
||||
|
||||
const [displayName, setDisplayName] = useState("");
|
||||
const [username, setUsername] = useState("");
|
||||
|
||||
const [displayNameChangeError, setDisplayNameChangeError] = useState<string | undefined>(undefined);
|
||||
const [usernameChangeError, setUsernameChangeError] = useState<string | undefined>(undefined);
|
||||
|
||||
const usernameDate = dayjs().add(90, "days");
|
||||
|
||||
const handleSubmitDisplayNameChange = async (close: () => void) => {
|
||||
const parsed = displayNameSchema.safeParse(displayName);
|
||||
if (!parsed.success) {
|
||||
setDisplayNameChangeError(parsed.error.errors[0].message);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch("/api/auth/display-name", {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ displayName }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const { error } = await response.json();
|
||||
setDisplayNameChangeError(error);
|
||||
return;
|
||||
}
|
||||
|
||||
close();
|
||||
router.refresh();
|
||||
};
|
||||
|
||||
const handleSubmitUsernameChange = async (close: () => void) => {
|
||||
const parsed = usernameSchema.safeParse(username);
|
||||
if (!parsed.success) {
|
||||
setUsernameChangeError(parsed.error.errors[0].message);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch("/api/auth/username", {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const { error } = await response.json();
|
||||
setUsernameChangeError(error);
|
||||
return;
|
||||
}
|
||||
|
||||
close();
|
||||
router.refresh();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-amber-50 border-2 border-amber-500 rounded-2xl p-4 flex flex-col gap-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold">Profile Settings</h2>
|
||||
<p className="text-sm text-zinc-500">Update your account info, username, and preferences.</p>
|
||||
<p className="text-sm text-zinc-500">Update your account info, and username.</p>
|
||||
</div>
|
||||
|
||||
{/* Separator */}
|
||||
|
|
@ -37,15 +90,22 @@ export default function ProfileSettings({ name, username }: Props) {
|
|||
</div>
|
||||
|
||||
<div className="flex justify-end gap-1">
|
||||
<input type="text" className="pill input w-full max-w-64" value={displayName} onChange={(e) => setDisplayName(e.target.value)} />
|
||||
<input
|
||||
type="text"
|
||||
className="pill input w-full max-w-64"
|
||||
placeholder="Type here..."
|
||||
value={displayName}
|
||||
onChange={(e) => setDisplayName(e.target.value)}
|
||||
/>
|
||||
<SubmitDialogButton
|
||||
title="Confirm Display Name Change"
|
||||
description="Update your display name? This will only be visible on your profile. You can change it again later."
|
||||
onSubmit={() => {}}
|
||||
error={displayNameChangeError}
|
||||
onSubmit={handleSubmitDisplayNameChange}
|
||||
>
|
||||
<div className="bg-orange-100 rounded-xl border-2 border-orange-400 mt-4 px-2 py-1">
|
||||
<p className="font-semibold">New display name:</p>
|
||||
<p className="indent-4">"{name}"</p>
|
||||
<p className="indent-4">'{displayName}'</p>
|
||||
</div>
|
||||
</SubmitDialogButton>
|
||||
</div>
|
||||
|
|
@ -65,19 +125,26 @@ export default function ProfileSettings({ name, username }: Props) {
|
|||
<input
|
||||
type="text"
|
||||
className="pill input w-full max-w-64 indent-4"
|
||||
value={usernameState}
|
||||
onChange={(e) => setUsernameState(e.target.value)}
|
||||
placeholder="Type here..."
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
<span className="absolute top-1/2 -translate-y-1/2 left-4 select-none">@</span>
|
||||
</div>
|
||||
<SubmitDialogButton
|
||||
title="Confirm Username Change"
|
||||
description="Are you sure? Your username is your unique indentifier and can only be changed every 90 days."
|
||||
onSubmit={() => {}}
|
||||
error={usernameChangeError}
|
||||
onSubmit={handleSubmitUsernameChange}
|
||||
>
|
||||
<p className="text-sm text-zinc-500 mt-2">
|
||||
After submitting, you can change it again on{" "}
|
||||
{usernameDate.toDate().toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" })}.
|
||||
</p>
|
||||
|
||||
<div className="bg-orange-100 rounded-xl border-2 border-orange-400 mt-4 px-2 py-1">
|
||||
<p className="font-semibold">New username:</p>
|
||||
<p className="indent-4">"@{usernameState}"</p>
|
||||
<p className="indent-4">'@{username}'</p>
|
||||
</div>
|
||||
</SubmitDialogButton>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { Icon } from "@iconify/react";
|
|||
interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
onSubmit: () => void;
|
||||
onSubmit: (close: () => void) => void;
|
||||
error?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
|
@ -17,9 +17,7 @@ export default function SubmitDialogButton({ title, description, onSubmit, error
|
|||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
const submit = () => {
|
||||
onSubmit();
|
||||
close();
|
||||
window.location.reload(); // I would use router.refresh() here but the API data fetching breaks
|
||||
onSubmit(close);
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { FormEvent, useState } from "react";
|
||||
import { redirect } from "next/navigation";
|
||||
import { usernameSchema } from "@/lib/schemas";
|
||||
|
||||
export default function UsernameForm() {
|
||||
const [username, setUsername] = useState("");
|
||||
|
|
@ -10,6 +11,9 @@ export default function UsernameForm() {
|
|||
const handleSubmit = async (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
const parsed = usernameSchema.safeParse(username);
|
||||
if (!parsed.success) setError(parsed.error.errors[0].message);
|
||||
|
||||
const response = await fetch("/api/auth/username", {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ export default async function ProfileSettingsPage() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<ProfileSettings name={session.user.name} username={session.user.username} />
|
||||
<ProfileSettings />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue