feat: usernames
also change userId to number
This commit is contained in:
parent
9344f2f315
commit
9d35d93d9e
15 changed files with 200 additions and 78 deletions
|
|
@ -10,12 +10,13 @@
|
||||||
"postinstall": "prisma generate"
|
"postinstall": "prisma generate"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/prisma-adapter": "^2.8.0",
|
"@auth/prisma-adapter": "2.7.2",
|
||||||
"@prisma/client": "^6.5.0",
|
"@prisma/client": "^6.5.0",
|
||||||
"next": "15.2.4",
|
"next": "15.2.4",
|
||||||
"next-auth": "5.0.0-beta.25",
|
"next-auth": "5.0.0-beta.25",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0"
|
"react-dom": "^19.0.0",
|
||||||
|
"zod": "^3.24.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ importers:
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@auth/prisma-adapter':
|
'@auth/prisma-adapter':
|
||||||
specifier: ^2.8.0
|
specifier: 2.7.2
|
||||||
version: 2.8.0(@prisma/client@6.5.0(prisma@6.5.0(typescript@5.8.2))(typescript@5.8.2))
|
version: 2.7.2(@prisma/client@6.5.0(prisma@6.5.0(typescript@5.8.2))(typescript@5.8.2))
|
||||||
'@prisma/client':
|
'@prisma/client':
|
||||||
specifier: ^6.5.0
|
specifier: ^6.5.0
|
||||||
version: 6.5.0(prisma@6.5.0(typescript@5.8.2))(typescript@5.8.2)
|
version: 6.5.0(prisma@6.5.0(typescript@5.8.2))(typescript@5.8.2)
|
||||||
|
|
@ -26,6 +26,9 @@ importers:
|
||||||
react-dom:
|
react-dom:
|
||||||
specifier: ^19.0.0
|
specifier: ^19.0.0
|
||||||
version: 19.0.0(react@19.0.0)
|
version: 19.0.0(react@19.0.0)
|
||||||
|
zod:
|
||||||
|
specifier: ^3.24.2
|
||||||
|
version: 3.24.2
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@eslint/eslintrc':
|
'@eslint/eslintrc':
|
||||||
specifier: ^3
|
specifier: ^3
|
||||||
|
|
@ -81,24 +84,10 @@ packages:
|
||||||
nodemailer:
|
nodemailer:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@auth/core@0.38.0':
|
'@auth/prisma-adapter@2.7.2':
|
||||||
resolution: {integrity: sha512-ClHl44x4cY3wfJmHLpW+XrYqED0fZIzbHmwbExltzroCjR5ts3DLTWzADRba8mJFYZ8JIEJDa+lXnGl0E9Bl7Q==}
|
resolution: {integrity: sha512-orznIVt6aQMoJ4/rfWFSpRPU8LoZn6jVtDuEkZgLud2xSnCalq6x+hX+rqlk4E5LM13NW1GIJojOPQnM4aM4Gw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@simplewebauthn/browser': ^9.0.1
|
'@prisma/client': '>=2.26.0 || >=3 || >=4 || >=5'
|
||||||
'@simplewebauthn/server': ^9.0.2
|
|
||||||
nodemailer: ^6.8.0
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@simplewebauthn/browser':
|
|
||||||
optional: true
|
|
||||||
'@simplewebauthn/server':
|
|
||||||
optional: true
|
|
||||||
nodemailer:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@auth/prisma-adapter@2.8.0':
|
|
||||||
resolution: {integrity: sha512-g0Bmq3l5xUDyBBiDgm/y3Zqb582CnRHzFqbloV7scrLia5AbVC0xy+ntn+CQCAWW9ibpwiqJrQKKboIWN1oGqw==}
|
|
||||||
peerDependencies:
|
|
||||||
'@prisma/client': '>=2.26.0 || >=3 || >=4 || >=5 || >=6'
|
|
||||||
|
|
||||||
'@emnapi/core@1.4.0':
|
'@emnapi/core@1.4.0':
|
||||||
resolution: {integrity: sha512-H+N/FqT07NmLmt6OFFtDfwe8PNygprzBikrEMyQfgqSmT0vzE515Pz7R8izwB9q/zsH/MA64AKoul3sA6/CzVg==}
|
resolution: {integrity: sha512-H+N/FqT07NmLmt6OFFtDfwe8PNygprzBikrEMyQfgqSmT0vzE515Pz7R8izwB9q/zsH/MA64AKoul3sA6/CzVg==}
|
||||||
|
|
@ -1411,9 +1400,6 @@ packages:
|
||||||
jose@5.10.0:
|
jose@5.10.0:
|
||||||
resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==}
|
resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==}
|
||||||
|
|
||||||
jose@6.0.10:
|
|
||||||
resolution: {integrity: sha512-skIAxZqcMkOrSwjJvplIPYrlXGpxTPnro2/QWTDCxAdWQrSTV5/KqspMWmi5WAx5+ULswASJiZ0a+1B/Lxt9cw==}
|
|
||||||
|
|
||||||
js-tokens@4.0.0:
|
js-tokens@4.0.0:
|
||||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||||
|
|
||||||
|
|
@ -1691,17 +1677,9 @@ packages:
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
preact: '>=10'
|
preact: '>=10'
|
||||||
|
|
||||||
preact-render-to-string@6.5.11:
|
|
||||||
resolution: {integrity: sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==}
|
|
||||||
peerDependencies:
|
|
||||||
preact: '>=10'
|
|
||||||
|
|
||||||
preact@10.11.3:
|
preact@10.11.3:
|
||||||
resolution: {integrity: sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==}
|
resolution: {integrity: sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==}
|
||||||
|
|
||||||
preact@10.24.3:
|
|
||||||
resolution: {integrity: sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==}
|
|
||||||
|
|
||||||
prelude-ls@1.2.1:
|
prelude-ls@1.2.1:
|
||||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
@ -1996,6 +1974,9 @@ packages:
|
||||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
zod@3.24.2:
|
||||||
|
resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==}
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
'@alloc/quick-lru@5.2.0': {}
|
'@alloc/quick-lru@5.2.0': {}
|
||||||
|
|
@ -2010,17 +1991,9 @@ snapshots:
|
||||||
preact: 10.11.3
|
preact: 10.11.3
|
||||||
preact-render-to-string: 5.2.3(preact@10.11.3)
|
preact-render-to-string: 5.2.3(preact@10.11.3)
|
||||||
|
|
||||||
'@auth/core@0.38.0':
|
'@auth/prisma-adapter@2.7.2(@prisma/client@6.5.0(prisma@6.5.0(typescript@5.8.2))(typescript@5.8.2))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@panva/hkdf': 1.2.1
|
'@auth/core': 0.37.2
|
||||||
jose: 6.0.10
|
|
||||||
oauth4webapi: 3.3.2
|
|
||||||
preact: 10.24.3
|
|
||||||
preact-render-to-string: 6.5.11(preact@10.24.3)
|
|
||||||
|
|
||||||
'@auth/prisma-adapter@2.8.0(@prisma/client@6.5.0(prisma@6.5.0(typescript@5.8.2))(typescript@5.8.2))':
|
|
||||||
dependencies:
|
|
||||||
'@auth/core': 0.38.0
|
|
||||||
'@prisma/client': 6.5.0(prisma@6.5.0(typescript@5.8.2))(typescript@5.8.2)
|
'@prisma/client': 6.5.0(prisma@6.5.0(typescript@5.8.2))(typescript@5.8.2)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@simplewebauthn/browser'
|
- '@simplewebauthn/browser'
|
||||||
|
|
@ -3417,8 +3390,6 @@ snapshots:
|
||||||
|
|
||||||
jose@5.10.0: {}
|
jose@5.10.0: {}
|
||||||
|
|
||||||
jose@6.0.10: {}
|
|
||||||
|
|
||||||
js-tokens@4.0.0: {}
|
js-tokens@4.0.0: {}
|
||||||
|
|
||||||
js-yaml@4.1.0:
|
js-yaml@4.1.0:
|
||||||
|
|
@ -3670,14 +3641,8 @@ snapshots:
|
||||||
preact: 10.11.3
|
preact: 10.11.3
|
||||||
pretty-format: 3.8.0
|
pretty-format: 3.8.0
|
||||||
|
|
||||||
preact-render-to-string@6.5.11(preact@10.24.3):
|
|
||||||
dependencies:
|
|
||||||
preact: 10.24.3
|
|
||||||
|
|
||||||
preact@10.11.3: {}
|
preact@10.11.3: {}
|
||||||
|
|
||||||
preact@10.24.3: {}
|
|
||||||
|
|
||||||
prelude-ls@1.2.1: {}
|
prelude-ls@1.2.1: {}
|
||||||
|
|
||||||
pretty-format@3.8.0: {}
|
pretty-format@3.8.0: {}
|
||||||
|
|
@ -4081,3 +4046,5 @@ snapshots:
|
||||||
word-wrap@1.2.5: {}
|
word-wrap@1.2.5: {}
|
||||||
|
|
||||||
yocto-queue@0.1.0: {}
|
yocto-queue@0.1.0: {}
|
||||||
|
|
||||||
|
zod@3.24.2: {}
|
||||||
|
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
-- AlterTable
|
|
||||||
ALTER TABLE "miis" ADD COLUMN "tags" TEXT[];
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE "users" (
|
CREATE TABLE "users" (
|
||||||
"id" TEXT NOT NULL,
|
"id" SERIAL NOT NULL,
|
||||||
"name" TEXT,
|
"username" TEXT,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
"email" TEXT NOT NULL,
|
"email" TEXT NOT NULL,
|
||||||
"emailVerified" TIMESTAMP(3),
|
"emailVerified" TIMESTAMP(3),
|
||||||
"image" TEXT,
|
"image" TEXT,
|
||||||
|
|
@ -13,7 +14,7 @@ CREATE TABLE "users" (
|
||||||
|
|
||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE "accounts" (
|
CREATE TABLE "accounts" (
|
||||||
"userId" TEXT NOT NULL,
|
"userId" INTEGER NOT NULL,
|
||||||
"type" TEXT NOT NULL,
|
"type" TEXT NOT NULL,
|
||||||
"provider" TEXT NOT NULL,
|
"provider" TEXT NOT NULL,
|
||||||
"providerAccountId" TEXT NOT NULL,
|
"providerAccountId" TEXT NOT NULL,
|
||||||
|
|
@ -33,7 +34,7 @@ CREATE TABLE "accounts" (
|
||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE "sessions" (
|
CREATE TABLE "sessions" (
|
||||||
"sessionToken" TEXT NOT NULL,
|
"sessionToken" TEXT NOT NULL,
|
||||||
"userId" TEXT NOT NULL,
|
"userId" INTEGER NOT NULL,
|
||||||
"expires" TIMESTAMP(3) NOT NULL,
|
"expires" TIMESTAMP(3) NOT NULL,
|
||||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
"updatedAt" TIMESTAMP(3) NOT NULL
|
"updatedAt" TIMESTAMP(3) NOT NULL
|
||||||
|
|
@ -41,10 +42,11 @@ CREATE TABLE "sessions" (
|
||||||
|
|
||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE "miis" (
|
CREATE TABLE "miis" (
|
||||||
"id" BIGSERIAL NOT NULL,
|
"id" SERIAL NOT NULL,
|
||||||
"userId" TEXT NOT NULL,
|
"userId" INTEGER NOT NULL,
|
||||||
"name" TEXT NOT NULL,
|
"name" VARCHAR(64) NOT NULL,
|
||||||
"pictures" TEXT[],
|
"pictures" TEXT[],
|
||||||
|
"tags" TEXT[],
|
||||||
"likes" INTEGER NOT NULL DEFAULT 0,
|
"likes" INTEGER NOT NULL DEFAULT 0,
|
||||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
|
@ -53,12 +55,15 @@ CREATE TABLE "miis" (
|
||||||
|
|
||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE "likes" (
|
CREATE TABLE "likes" (
|
||||||
"userId" TEXT NOT NULL,
|
"userId" INTEGER NOT NULL,
|
||||||
"miiId" BIGINT NOT NULL,
|
"miiId" INTEGER NOT NULL,
|
||||||
|
|
||||||
CONSTRAINT "likes_pkey" PRIMARY KEY ("userId","miiId")
|
CONSTRAINT "likes_pkey" PRIMARY KEY ("userId","miiId")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "users_username_key" ON "users"("username");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
|
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
|
||||||
|
|
||||||
|
|
@ -8,8 +8,9 @@ datasource db {
|
||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id String @id @default(cuid())
|
id Int @id @default(autoincrement())
|
||||||
name String?
|
username String? @unique
|
||||||
|
name String
|
||||||
email String @unique
|
email String @unique
|
||||||
emailVerified DateTime?
|
emailVerified DateTime?
|
||||||
image String?
|
image String?
|
||||||
|
|
@ -26,7 +27,7 @@ model User {
|
||||||
}
|
}
|
||||||
|
|
||||||
model Account {
|
model Account {
|
||||||
userId String
|
userId Int
|
||||||
type String
|
type String
|
||||||
provider String
|
provider String
|
||||||
providerAccountId String
|
providerAccountId String
|
||||||
|
|
@ -49,7 +50,7 @@ model Account {
|
||||||
|
|
||||||
model Session {
|
model Session {
|
||||||
sessionToken String @unique
|
sessionToken String @unique
|
||||||
userId String
|
userId Int
|
||||||
expires DateTime
|
expires DateTime
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
|
@ -60,8 +61,8 @@ model Session {
|
||||||
}
|
}
|
||||||
|
|
||||||
model Mii {
|
model Mii {
|
||||||
id BigInt @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
userId String
|
userId Int
|
||||||
name String @db.VarChar(64)
|
name String @db.VarChar(64)
|
||||||
pictures String[]
|
pictures String[]
|
||||||
tags String[]
|
tags String[]
|
||||||
|
|
@ -76,8 +77,8 @@ model Mii {
|
||||||
}
|
}
|
||||||
|
|
||||||
model Like {
|
model Like {
|
||||||
userId String
|
userId Int
|
||||||
miiId BigInt
|
miiId Int
|
||||||
|
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
mii Mii @relation(fields: [miiId], references: [id], onDelete: Cascade)
|
mii Mii @relation(fields: [miiId], references: [id], onDelete: Cascade)
|
||||||
|
|
|
||||||
38
src/app/api/auth/username/route.ts
Normal file
38
src/app/api/auth/username/route.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
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");
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const session = await auth();
|
||||||
|
if (!session) return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
|
||||||
|
return Response.json({ username: session.user.username });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function PATCH(request: Request) {
|
||||||
|
const session = await auth();
|
||||||
|
if (!session) return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
|
||||||
|
const { username } = await request.json();
|
||||||
|
if (!username) return Response.json({ error: "Username is required" }, { status: 400 });
|
||||||
|
|
||||||
|
const validation = usernameSchema.safeParse(username);
|
||||||
|
if (!validation.success) return Response.json({ error: validation.error.errors[0].message }, { status: 400 });
|
||||||
|
|
||||||
|
const existingUser = await prisma.user.findUnique({ where: { username } });
|
||||||
|
if (existingUser) return Response.json({ error: "Username is already taken" }, { status: 400 });
|
||||||
|
|
||||||
|
await prisma.user.update({
|
||||||
|
where: { email: session.user?.email ?? undefined },
|
||||||
|
data: { username },
|
||||||
|
});
|
||||||
|
|
||||||
|
return Response.json({ success: true });
|
||||||
|
}
|
||||||
|
|
@ -23,7 +23,7 @@ export default function LikeButton({ likes, isLoggedIn }: Props) {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button onClick={onClick} className="flex items-center gap-2 text-xl text-red-400 mt-1 cursor-pointer">
|
<button onClick={onClick} className="flex items-center gap-2 text-xl text-red-400 cursor-pointer">
|
||||||
<Icon icon={isLiked ? "icon-park-solid:like" : "icon-park-outline:like"} />
|
<Icon icon={isLiked ? "icon-park-solid:like" : "icon-park-outline:like"} />
|
||||||
<span>{likesState}</span>
|
<span>{likesState}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,17 @@ import { signIn } from "next-auth/react";
|
||||||
export default function LoginButtons() {
|
export default function LoginButtons() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center gap-2 mt-8">
|
<div className="flex flex-col items-center gap-2 mt-8">
|
||||||
<button onClick={() => signIn("discord")} className="pill button gap-2 !px-3 !bg-indigo-400 !border-indigo-500 hover:!bg-indigo-500">
|
<button
|
||||||
|
onClick={() => signIn("discord", { redirectTo: "/create-username" })}
|
||||||
|
className="pill button gap-2 !px-3 !bg-indigo-400 !border-indigo-500 hover:!bg-indigo-500"
|
||||||
|
>
|
||||||
<Icon icon="ic:baseline-discord" fontSize={32} />
|
<Icon icon="ic:baseline-discord" fontSize={32} />
|
||||||
Login with Discord
|
Login with Discord
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => signIn("github")} className="pill button gap-2 !px-3 !bg-zinc-700 !border-zinc-800 hover:!bg-zinc-800 text-white">
|
<button
|
||||||
|
onClick={() => signIn("github", { redirectTo: "/create-username" })}
|
||||||
|
className="pill button gap-2 !px-3 !bg-zinc-700 !border-zinc-800 hover:!bg-zinc-800 text-white"
|
||||||
|
>
|
||||||
<Icon icon="mdi:github" fontSize={32} />
|
<Icon icon="mdi:github" fontSize={32} />
|
||||||
Login with GitHub
|
Login with GitHub
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ export default async function ProfileOverview() {
|
||||||
<li title="Your profile">
|
<li title="Your profile">
|
||||||
<button className="pill button !gap-2 !p-0 h-full max-w-64">
|
<button className="pill button !gap-2 !p-0 h-full max-w-64">
|
||||||
<img src={session?.user?.image ?? "/missing.webp"} alt="profile picture" className="rounded-full h-full outline-2 outline-orange-400" />
|
<img src={session?.user?.image ?? "/missing.webp"} alt="profile picture" className="rounded-full h-full outline-2 outline-orange-400" />
|
||||||
<span className="pr-4 overflow-hidden whitespace-nowrap text-ellipsis w-full">{session?.user?.name}</span>
|
<span className="pr-4 overflow-hidden whitespace-nowrap text-ellipsis w-full">{session?.user?.username ?? "unknown"}</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
45
src/app/components/username-form.tsx
Normal file
45
src/app/components/username-form.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { FormEvent, useState } from "react";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
|
export default function UsernameForm() {
|
||||||
|
const [username, setUsername] = useState("");
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
const handleSubmit = async (event: FormEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
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 onSubmit={handleSubmit} className="flex flex-col items-center">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Type your username..."
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
required
|
||||||
|
className="pill !bg-orange-200 outline-0 focus:ring-[3px] ring-orange-400/50 transition w-96 mt-8 mb-2"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button type="submit" className="pill button w-min">
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
{error && <p className="text-red-400 font-semibold mt-4">Error: {error}</p>}
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
22
src/app/create-username/page.tsx
Normal file
22
src/app/create-username/page.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { auth } from "@/lib/auth";
|
||||||
|
|
||||||
|
import UsernameForm from "../components/username-form";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
|
export default async function CreateUsernamePage() {
|
||||||
|
const session = await auth();
|
||||||
|
|
||||||
|
// If the user is not logged in or already has a username, redirect
|
||||||
|
if (!session || session?.user.username) {
|
||||||
|
redirect("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-medium text-center">Welcome to TomodachiShare!</h1>
|
||||||
|
<h2 className="text-lg text-center">Please create a username</h2>
|
||||||
|
|
||||||
|
<UsernameForm />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export { auth as middleware } from "@/lib/auth";
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
|
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
|
|
@ -36,8 +37,19 @@ export default async function Page({ searchParams }: { searchParams: Promise<{ [
|
||||||
const miis = await prisma.mii.findMany({
|
const miis = await prisma.mii.findMany({
|
||||||
where: where,
|
where: where,
|
||||||
orderBy,
|
orderBy,
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
username: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (session?.user && !session.user.username) {
|
||||||
|
redirect("/create-username");
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="flex justify-between items-end mb-2">
|
<div className="flex justify-between items-end mb-2">
|
||||||
|
|
@ -74,20 +86,24 @@ export default async function Page({ searchParams }: { searchParams: Promise<{ [
|
||||||
{miis.map((mii) => (
|
{miis.map((mii) => (
|
||||||
<div
|
<div
|
||||||
key={mii.id}
|
key={mii.id}
|
||||||
className="bg-zinc-50 rounded-3xl border-2 border-zinc-300 shadow-lg p-3 transition hover:scale-105 hover:bg-cyan-100 hover:border-cyan-600"
|
className="flex flex-col bg-zinc-50 rounded-3xl border-2 border-zinc-300 shadow-lg p-3 transition hover:scale-105 hover:bg-cyan-100 hover:border-cyan-600"
|
||||||
>
|
>
|
||||||
<img src="https://placehold.co/600x400" alt="mii" className="rounded-xl" />
|
<img src="https://placehold.co/600x400" alt="mii" className="rounded-xl" />
|
||||||
<div className="p-4">
|
<div className="p-4 flex flex-col gap-1 h-full">
|
||||||
<h3 className="font-bold text-2xl overflow-hidden text-ellipsis line-clamp-2" title={mii.name}>
|
<h3 className="font-bold text-2xl overflow-hidden text-ellipsis line-clamp-2" title={mii.name}>
|
||||||
{mii.name}
|
{mii.name}
|
||||||
</h3>
|
</h3>
|
||||||
<div id="tags" className="flex gap-1 mt-1 *:px-2 *:py-1 *:bg-orange-300 *:rounded-full *:text-xs">
|
<div id="tags" className="flex gap-1 *:px-2 *:py-1 *:bg-orange-300 *:rounded-full *:text-xs">
|
||||||
{mii.tags.map((tag) => (
|
{mii.tags.map((tag) => (
|
||||||
<span key={tag}>{tag}</span>
|
<span key={tag}>{tag}</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-auto grid grid-cols-2 items-center">
|
||||||
<LikeButton likes={mii.likes} isLoggedIn={session?.user != null} />
|
<LikeButton likes={mii.likes} isLoggedIn={session?.user != null} />
|
||||||
|
|
||||||
|
<span className="text-sm text-right text-ellipsis">@{mii.user?.username}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -11,4 +11,14 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
||||||
pages: {
|
pages: {
|
||||||
signIn: "/login",
|
signIn: "/login",
|
||||||
},
|
},
|
||||||
|
callbacks: {
|
||||||
|
async session({ session, user }) {
|
||||||
|
if (user) {
|
||||||
|
session.user.id = user.id;
|
||||||
|
session.user.username = user.username;
|
||||||
|
session.user.email = user.email;
|
||||||
|
}
|
||||||
|
return session;
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
14
src/types.d.ts
vendored
Normal file
14
src/types.d.ts
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { DefaultSession, User } from "next-auth";
|
||||||
|
import { User as PrismaUser } from "@prisma/client";
|
||||||
|
|
||||||
|
declare module "next-auth" {
|
||||||
|
interface Session {
|
||||||
|
user: {
|
||||||
|
username?: string;
|
||||||
|
} & DefaultSession["user"];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
username?: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue