mirror of
https://github.com/trafficlunar/tomodachi-share.git
synced 2026-06-28 14:44:15 +00:00
Merge branch 'main' into feat/living-the-dream-manually
This commit is contained in:
commit
22911804c0
56 changed files with 1166 additions and 1669 deletions
|
|
@ -14,7 +14,6 @@ import PunishmentDeletionDialog from "./punishment-deletion-dialog";
|
|||
interface ApiResponse {
|
||||
success: boolean;
|
||||
name: string;
|
||||
username: string;
|
||||
image: string;
|
||||
createdAt: string;
|
||||
punishments: Prisma.PunishmentGetPayload<{
|
||||
|
|
@ -115,7 +114,7 @@ export default function Punishments() {
|
|||
<ProfilePicture src={user.image} width={96} height={96} className="rounded-full border-2 border-orange-400" />
|
||||
<div className="p-2 flex flex-col">
|
||||
<p className="text-xl font-bold">{user.name}</p>
|
||||
<p className="text-black/60 text-sm font-medium">@{user.username}</p>
|
||||
<p className="text-black/60 text-sm font-medium">@{user.name}</p>
|
||||
<p className="text-sm mt-auto">
|
||||
<span className="font-medium">Created:</span>{" "}
|
||||
{new Date(user.createdAt).toLocaleString("en-GB", {
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ export default function Description({ text, className }: Props) {
|
|||
href={`/profile/${id}`}
|
||||
className="inline-flex items-center align-bottom gap-1.5 pr-2 bg-orange-100 border border-orange-400 rounded-lg mx-1 text-orange-800 text-xs"
|
||||
>
|
||||
<ProfilePicture src={linkedProfile.image || "/guest.webp"} width={24} height={24} className="bg-white rounded-lg border-r border-orange-400" />
|
||||
<ProfilePicture src={linkedProfile.image || "/guest.png"} width={24} height={24} className="bg-white rounded-lg border-r border-orange-400" />
|
||||
{linkedProfile.name}
|
||||
</Link>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export default function Dropzone({ onDrop, options, children }: Props) {
|
|||
onDrop: handleDrop,
|
||||
maxFiles: 3,
|
||||
accept: {
|
||||
"image/*": [".png", ".jpg", ".jpeg", ".bmp", ".webp", ".heic"],
|
||||
"image/*": [".png", ".jpg", ".jpeg", ".bmp", ".png", ".heic"],
|
||||
},
|
||||
...options,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export default function LoginButtons() {
|
|||
return (
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<button
|
||||
onClick={() => signIn("discord", { redirectTo: "/create-username" })}
|
||||
onClick={() => signIn("discord", { redirectTo: "/" })}
|
||||
aria-label="Login with Discord"
|
||||
className="pill button gap-2 px-3! bg-indigo-400! border-indigo-500! hover:bg-indigo-500!"
|
||||
>
|
||||
|
|
@ -15,7 +15,7 @@ export default function LoginButtons() {
|
|||
Login with Discord
|
||||
</button>
|
||||
<button
|
||||
onClick={() => signIn("github", { redirectTo: "/create-username" })}
|
||||
onClick={() => signIn("github", { redirectTo: "/" })}
|
||||
aria-label="Login with GitHub"
|
||||
className="pill button gap-2 px-3! bg-zinc-700! border-zinc-800! hover:bg-zinc-800! text-white"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { headers } from "next/headers";
|
||||
import Link from "next/link";
|
||||
|
||||
import { MiiGender, MiiPlatform, Prisma } from "@prisma/client";
|
||||
|
|
@ -25,7 +26,6 @@ interface Props {
|
|||
|
||||
export default async function MiiList({ searchParams, userId, inLikesPage }: Props) {
|
||||
const session = await auth();
|
||||
|
||||
const parsed = searchSchema.safeParse(searchParams);
|
||||
if (!parsed.success) return <h1>{parsed.error.issues[0].message}</h1>;
|
||||
|
||||
|
|
@ -34,7 +34,7 @@ export default async function MiiList({ searchParams, userId, inLikesPage }: Pro
|
|||
// My Likes page
|
||||
let miiIdsLiked: number[] | undefined = undefined;
|
||||
|
||||
if (inLikesPage && session?.user.id) {
|
||||
if (inLikesPage && session?.user?.id) {
|
||||
const likedMiis = await prisma.like.findMany({
|
||||
where: { userId: Number(session.user.id) },
|
||||
select: { miiId: true },
|
||||
|
|
@ -69,7 +69,7 @@ export default async function MiiList({ searchParams, userId, inLikesPage }: Pro
|
|||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
|
@ -219,11 +219,11 @@ export default async function MiiList({ searchParams, userId, inLikesPage }: Pro
|
|||
|
||||
{!userId && (
|
||||
<Link href={`/profile/${mii.user?.id}`} className="text-sm text-right overflow-hidden text-ellipsis">
|
||||
@{mii.user?.username}
|
||||
@{mii.user?.name}
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{userId && Number(session?.user.id) == userId && (
|
||||
{userId && Number(session?.user?.id) == userId && (
|
||||
<div className="flex gap-1 text-2xl justify-end text-zinc-400">
|
||||
<Link href={`/edit/${mii.id}`} title="Edit Mii" aria-label="Edit Mii" data-tooltip="Edit">
|
||||
<Icon icon="mdi:pencil" />
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ interface Props {
|
|||
export default async function ProfileInformation({ userId, page }: Props) {
|
||||
const session = await auth();
|
||||
|
||||
const id = userId ? userId : Number(session?.user.id);
|
||||
const id = userId ? userId : Number(session?.user?.id);
|
||||
const user = await prisma.user.findUnique({ where: { id } });
|
||||
const likedMiis = await prisma.like.count({ where: { userId: id } });
|
||||
|
||||
|
|
@ -23,14 +23,14 @@ export default async function ProfileInformation({ userId, page }: Props) {
|
|||
|
||||
const isAdmin = id === Number(process.env.NEXT_PUBLIC_ADMIN_USER_ID);
|
||||
const isContributor = process.env.NEXT_PUBLIC_CONTRIBUTORS_USER_IDS?.split(",").includes(id.toString());
|
||||
const isOwnProfile = Number(session?.user.id) === id;
|
||||
const isOwnProfile = Number(session?.user?.id) === id;
|
||||
|
||||
return (
|
||||
<div className="bg-amber-50 border-2 border-amber-500 rounded-2xl shadow-lg p-4 flex gap-4 mb-2 max-md:flex-col">
|
||||
<div className="flex w-full gap-4 overflow-x-scroll">
|
||||
{/* Profile picture */}
|
||||
<Link href={`/profile/${user.id}`} className="size-28 aspect-square">
|
||||
<ProfilePicture src={user.image ?? "/guest.webp"} className="rounded-full bg-white border-2 border-orange-400 shadow max-md:self-center" />
|
||||
<ProfilePicture src={user.image ?? "/guest.png"} className="rounded-full bg-white border-2 border-orange-400 shadow max-md:self-center" />
|
||||
</Link>
|
||||
{/* User information */}
|
||||
<div className="flex flex-col w-full relative py-3">
|
||||
|
|
@ -47,7 +47,7 @@ export default async function ProfileInformation({ userId, page }: Props) {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
<h2 className="text-black/60 text-sm font-semibold wrap-break-word">@{user?.username}</h2>
|
||||
<h2 className="text-black/60 text-sm font-semibold wrap-break-word">ID: {user?.id}</h2>
|
||||
|
||||
<div className="mt-3 text-sm flex gap-8">
|
||||
<h4 title={`${user.createdAt.toLocaleTimeString("en-GB", { timeZone: "UTC" })} UTC`}>
|
||||
|
|
|
|||
|
|
@ -7,15 +7,15 @@ export default async function ProfileOverview() {
|
|||
|
||||
return (
|
||||
<li title="Your profile">
|
||||
<Link href={`/profile/${session?.user.id}`} aria-label="Go to profile" className="pill button gap-2! p-0! h-full max-w-64" data-tooltip="Your Profile">
|
||||
<Link href={`/profile/${session?.user?.id}`} aria-label="Go to profile" className="pill button gap-2! p-0! h-full max-w-64" data-tooltip="Your Profile">
|
||||
<Image
|
||||
src={session?.user?.image ?? "/guest.webp"}
|
||||
src={session?.user?.image ?? "/guest.png"}
|
||||
alt="profile picture"
|
||||
width={40}
|
||||
height={40}
|
||||
className="rounded-full aspect-square object-cover h-full bg-white outline-2 outline-orange-400"
|
||||
/>
|
||||
<span className="pr-4 overflow-hidden whitespace-nowrap text-ellipsis w-full">{session?.user?.username ?? "unknown"}</span>
|
||||
<span className="pr-4 overflow-hidden whitespace-nowrap text-ellipsis w-full">{session?.user?.name ?? "unknown"}</span>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -7,5 +7,5 @@ export default function ProfilePicture(props: Partial<ImageProps>) {
|
|||
const { src, ...rest } = props;
|
||||
const [imgSrc, setImgSrc] = useState(src);
|
||||
|
||||
return <Image width={128} height={128} {...rest} src={imgSrc || "/guest.webp"} alt={"profile picture"} onError={() => setImgSrc("/guest.webp")} />;
|
||||
return <Image width={128} height={128} {...rest} src={imgSrc || "/guest.png"} alt={"profile picture"} onError={() => setImgSrc("/guest.png")} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,8 @@
|
|||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { displayNameSchema, usernameSchema } from "@/lib/schemas";
|
||||
import { userNameSchema } from "@/lib/schemas";
|
||||
|
||||
import ProfilePictureSettings from "./profile-picture";
|
||||
import SubmitDialogButton from "./submit-dialog-button";
|
||||
|
|
@ -19,14 +18,10 @@ export default function ProfileSettings({ currentDescription }: Props) {
|
|||
const router = useRouter();
|
||||
|
||||
const [description, setDescription] = useState(currentDescription);
|
||||
const [displayName, setDisplayName] = useState("");
|
||||
const [username, setUsername] = useState("");
|
||||
const [name, setName] = useState("");
|
||||
|
||||
const [descriptionChangeError, setDescriptionChangeError] = useState<string | undefined>(undefined);
|
||||
const [displayNameChangeError, setDisplayNameChangeError] = useState<string | undefined>(undefined);
|
||||
const [usernameChangeError, setUsernameChangeError] = useState<string | undefined>(undefined);
|
||||
|
||||
const usernameDate = dayjs().add(90, "days");
|
||||
const [nameChangeError, setNameChangeError] = useState<string | undefined>(undefined);
|
||||
|
||||
const handleSubmitDescriptionChange = async (close: () => void) => {
|
||||
const parsed = z.string().trim().max(256).safeParse(description);
|
||||
|
|
@ -51,45 +46,22 @@ export default function ProfileSettings({ currentDescription }: Props) {
|
|||
router.refresh();
|
||||
};
|
||||
|
||||
const handleSubmitDisplayNameChange = async (close: () => void) => {
|
||||
const parsed = displayNameSchema.safeParse(displayName);
|
||||
const handleSubmitNameChange = async (close: () => void) => {
|
||||
const parsed = userNameSchema.safeParse(name);
|
||||
if (!parsed.success) {
|
||||
setDisplayNameChangeError(parsed.error.issues[0].message);
|
||||
setNameChangeError(parsed.error.issues[0].message);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch("/api/auth/display-name", {
|
||||
const response = await fetch("/api/auth/name", {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ displayName }),
|
||||
body: JSON.stringify({ name }),
|
||||
});
|
||||
|
||||
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.issues[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);
|
||||
setNameChangeError(error);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -101,7 +73,7 @@ export default function ProfileSettings({ currentDescription }: Props) {
|
|||
<div className="bg-amber-50 border-2 border-amber-500 rounded-2xl shadow-lg 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, and username.</p>
|
||||
<p className="text-sm text-zinc-500">Update your profile picture, description, name, etc.</p>
|
||||
</div>
|
||||
|
||||
{/* Separator */}
|
||||
|
|
@ -146,58 +118,21 @@ export default function ProfileSettings({ currentDescription }: Props) {
|
|||
{/* Change Name */}
|
||||
<div className="grid grid-cols-5 gap-4 max-lg:grid-cols-1">
|
||||
<div className="col-span-3">
|
||||
<label className="font-semibold">Change Display Name</label>
|
||||
<p className="text-sm text-zinc-500">This is a display name shown on your profile — feel free to change it anytime</p>
|
||||
<label className="font-semibold">Change Name</label>
|
||||
<p className="text-sm text-zinc-500">This is your name shown on your profile and miis — feel free to change it anytime</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-1 h-min col-span-2">
|
||||
<input type="text" className="pill input flex-1" placeholder="Type here..." value={displayName} onChange={(e) => setDisplayName(e.target.value)} />
|
||||
<input type="text" className="pill input flex-1" placeholder="Type here..." value={name} onChange={(e) => setName(e.target.value)} />
|
||||
<SubmitDialogButton
|
||||
title="Confirm Display Name Change"
|
||||
description="Are you sure? This will only be visible on your profile. You can change it again later."
|
||||
error={displayNameChangeError}
|
||||
onSubmit={handleSubmitDisplayNameChange}
|
||||
title="Confirm Name Change"
|
||||
description="Are you sure? You can change it again later."
|
||||
error={nameChangeError}
|
||||
onSubmit={handleSubmitNameChange}
|
||||
>
|
||||
<div className="bg-orange-100 rounded-xl border-2 border-amber-500 mt-4 px-2 py-1">
|
||||
<p className="font-semibold">New display name:</p>
|
||||
<p className="indent-4">'{displayName}'</p>
|
||||
</div>
|
||||
</SubmitDialogButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Change Username */}
|
||||
<div className="grid grid-cols-5 gap-4 max-lg:grid-cols-1">
|
||||
<div className="col-span-3">
|
||||
<label className="font-semibold">Change Username</label>
|
||||
<p className="text-sm text-zinc-500">Your unique tag on the site. Can only be changed once every 90 days</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-1 col-span-2">
|
||||
<div className="relative flex-1">
|
||||
<input
|
||||
type="text"
|
||||
className="pill input w-full indent-4"
|
||||
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."
|
||||
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-amber-500 mt-4 px-2 py-1">
|
||||
<p className="font-semibold">New username:</p>
|
||||
<p className="indent-4">'@{username}'</p>
|
||||
<p className="font-semibold">New name:</p>
|
||||
<p className="indent-4">'{name}'</p>
|
||||
</div>
|
||||
</SubmitDialogButton>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ export default function ProfilePictureSettings() {
|
|||
</p>
|
||||
|
||||
<Image
|
||||
src={newPicture ? URL.createObjectURL(newPicture) : "/guest.webp"}
|
||||
src={newPicture ? URL.createObjectURL(newPicture) : "/guest.png"}
|
||||
alt="new profile picture"
|
||||
width={96}
|
||||
height={96}
|
||||
|
|
@ -93,7 +93,7 @@ export default function ProfilePictureSettings() {
|
|||
<div className="bg-orange-100 rounded-xl border-2 border-amber-500 mt-4 px-2 py-1 flex items-center">
|
||||
<p className="font-semibold mb-2">New profile picture:</p>
|
||||
<Image
|
||||
src={newPicture ? URL.createObjectURL(newPicture) : "/guest.webp"}
|
||||
src={newPicture ? URL.createObjectURL(newPicture) : "/guest.png"}
|
||||
alt="new profile picture"
|
||||
width={128}
|
||||
height={128}
|
||||
|
|
|
|||
|
|
@ -43,11 +43,8 @@ export default function ReportUserForm({ user }: Props) {
|
|||
<hr className="border-zinc-300" />
|
||||
|
||||
<div className="bg-orange-100 rounded-xl border-2 border-orange-400 flex p-4 gap-4">
|
||||
<ProfilePicture src={user.image ?? "/guest.webp"} width={96} height={96} className="aspect-square rounded-full border-2 border-orange-400" />
|
||||
<div className="flex flex-col justify-center">
|
||||
<p className="text-xl font-bold overflow-hidden text-ellipsis">{user.name}</p>
|
||||
<p className="text-sm font-bold overflow-hidden text-ellipsis">@{user.username}</p>
|
||||
</div>
|
||||
<ProfilePicture src={user.image ?? "/guest.png"} width={96} height={96} className="aspect-square rounded-full border-2 border-orange-400" />
|
||||
<p className="text-xl font-bold overflow-hidden text-ellipsis">{user.name}</p>
|
||||
</div>
|
||||
|
||||
<div className="w-full grid grid-cols-3 items-center">
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ export default function EditForm({ mii, likes }: Props) {
|
|||
const response = await fetch(path);
|
||||
const blob = await response.blob();
|
||||
|
||||
return Object.assign(new File([blob], `image${index}.webp`, { type: "image/webp" }), { path });
|
||||
return Object.assign(new File([blob], `image${index}.png`, { type: "image/png" }), { path });
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { redirect } from "next/navigation";
|
||||
import { usernameSchema } from "@/lib/schemas";
|
||||
import SubmitButton from "./submit-button";
|
||||
|
||||
export default function UsernameForm() {
|
||||
const [username, setUsername] = useState("");
|
||||
const [error, setError] = useState<string | undefined>(undefined);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const parsed = usernameSchema.safeParse(username);
|
||||
if (!parsed.success) setError(parsed.error.issues[0].message);
|
||||
|
||||
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();
|
||||
setError(error);
|
||||
return;
|
||||
}
|
||||
|
||||
redirect("/");
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="flex flex-col items-center">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Type your username..."
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
required
|
||||
className="pill input w-96 mb-2"
|
||||
/>
|
||||
|
||||
<SubmitButton onClick={handleSubmit} />
|
||||
{error && <p className="text-red-400 font-semibold mt-4">Error: {error}</p>}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue