feat: integrate get profile modal endpoint
This commit is contained in:
parent
b2008f693f
commit
1fcc44787b
|
|
@ -9,6 +9,7 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@base-ui/react": "^1.1.0",
|
"@base-ui/react": "^1.1.0",
|
||||||
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@next-auth/prisma-adapter": "^1.0.7",
|
"@next-auth/prisma-adapter": "^1.0.7",
|
||||||
"@prisma/adapter-pg": "^7.3.0",
|
"@prisma/adapter-pg": "^7.3.0",
|
||||||
"@prisma/client": "^7.3.0",
|
"@prisma/client": "^7.3.0",
|
||||||
|
|
@ -29,12 +30,14 @@
|
||||||
"radix-ui": "^1.4.3",
|
"radix-ui": "^1.4.3",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
|
"react-hook-form": "^7.71.1",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"recharts": "^3.7.0",
|
"recharts": "^3.7.0",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsx": "^4.21.0"
|
"tsx": "^4.21.0",
|
||||||
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@prisma/adapter-neon": "^7.3.0",
|
"@prisma/adapter-neon": "^7.3.0",
|
||||||
|
|
@ -1098,6 +1101,18 @@
|
||||||
"hono": "^4"
|
"hono": "^4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@hookform/resolvers": {
|
||||||
|
"version": "5.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz",
|
||||||
|
"integrity": "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@standard-schema/utils": "^0.3.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react-hook-form": "^7.55.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@humanfs/core": {
|
"node_modules/@humanfs/core": {
|
||||||
"version": "0.19.1",
|
"version": "0.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||||
|
|
@ -11880,6 +11895,22 @@
|
||||||
"react": "^19.2.3"
|
"react": "^19.2.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-hook-form": {
|
||||||
|
"version": "7.71.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.1.tgz",
|
||||||
|
"integrity": "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/react-hook-form"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17 || ^18 || ^19"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-icons": {
|
"node_modules/react-icons": {
|
||||||
"version": "5.5.0",
|
"version": "5.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
|
||||||
|
|
@ -13779,7 +13810,6 @@
|
||||||
"version": "4.3.6",
|
"version": "4.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
||||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@base-ui/react": "^1.1.0",
|
"@base-ui/react": "^1.1.0",
|
||||||
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@next-auth/prisma-adapter": "^1.0.7",
|
"@next-auth/prisma-adapter": "^1.0.7",
|
||||||
"@prisma/adapter-pg": "^7.3.0",
|
"@prisma/adapter-pg": "^7.3.0",
|
||||||
"@prisma/client": "^7.3.0",
|
"@prisma/client": "^7.3.0",
|
||||||
|
|
@ -31,12 +32,14 @@
|
||||||
"radix-ui": "^1.4.3",
|
"radix-ui": "^1.4.3",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
|
"react-hook-form": "^7.71.1",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"recharts": "^3.7.0",
|
"recharts": "^3.7.0",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsx": "^4.21.0"
|
"tsx": "^4.21.0",
|
||||||
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@prisma/adapter-neon": "^7.3.0",
|
"@prisma/adapter-neon": "^7.3.0",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,187 @@
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "UserGender" AS ENUM ('MALE', 'FEMALE', 'OTHER');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "Sentiment" AS ENUM ('POSITIVE', 'NEGATIVE', 'NEUTRAL');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "OS" AS ENUM ('WINDOWS', 'MACOS', 'LINUX', 'CHROME_OS', 'OTHER');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "Brand" AS ENUM ('APPLE', 'ASUS', 'ACER', 'LENOVO', 'HP', 'DELL', 'MSI', 'AXIOO', 'ADVAN', 'ZYREX', 'OTHER');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "Profession" AS ENUM ('PROGRAMMER', 'DESIGNER', 'STUDENT', 'GAMER', 'OTHER');
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Account" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"userId" INTEGER NOT NULL,
|
||||||
|
"type" TEXT NOT NULL,
|
||||||
|
"provider" TEXT NOT NULL,
|
||||||
|
"providerAccountId" TEXT NOT NULL,
|
||||||
|
"refresh_token" TEXT,
|
||||||
|
"access_token" TEXT,
|
||||||
|
"expires_at" INTEGER,
|
||||||
|
"token_type" TEXT,
|
||||||
|
"scope" TEXT,
|
||||||
|
"id_token" TEXT,
|
||||||
|
"session_state" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "Account_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Session" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"sessionToken" TEXT NOT NULL,
|
||||||
|
"userId" INTEGER NOT NULL,
|
||||||
|
"expires" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "VerificationToken" (
|
||||||
|
"identifier" TEXT NOT NULL,
|
||||||
|
"token" TEXT NOT NULL,
|
||||||
|
"expires" TIMESTAMP(3) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "User" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"name" TEXT,
|
||||||
|
"email" TEXT,
|
||||||
|
"emailVerified" TIMESTAMP(3),
|
||||||
|
"image" TEXT,
|
||||||
|
"password" TEXT,
|
||||||
|
"bio" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "UserPreference" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"profession" "Profession",
|
||||||
|
"preferredOS" "OS",
|
||||||
|
"preferedBrand" "Brand",
|
||||||
|
"budgetMin" INTEGER,
|
||||||
|
"budgetMax" INTEGER,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"userId" INTEGER NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "UserPreference_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Product" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"brand" TEXT,
|
||||||
|
"url" TEXT NOT NULL,
|
||||||
|
"image" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Product_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Review" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"content" TEXT NOT NULL,
|
||||||
|
"sentiment" "Sentiment" NOT NULL,
|
||||||
|
"confidenceScore" DOUBLE PRECISION NOT NULL,
|
||||||
|
"keywords" TEXT[],
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"productId" INTEGER NOT NULL,
|
||||||
|
"modelId" INTEGER,
|
||||||
|
"userId" INTEGER,
|
||||||
|
|
||||||
|
CONSTRAINT "Review_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Analysis" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"targetProfession" TEXT NOT NULL,
|
||||||
|
"generalSentiment" DOUBLE PRECISION NOT NULL,
|
||||||
|
"compatibilityScore" DOUBLE PRECISION NOT NULL,
|
||||||
|
"verdict" TEXT NOT NULL,
|
||||||
|
"topKeywords" TEXT[],
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"userId" INTEGER NOT NULL,
|
||||||
|
"productId" INTEGER NOT NULL,
|
||||||
|
"modelId" INTEGER NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Analysis_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Model" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"modelName" TEXT NOT NULL,
|
||||||
|
"description" TEXT,
|
||||||
|
"accuracy" DOUBLE PRECISION NOT NULL,
|
||||||
|
"macroF1" DOUBLE PRECISION NOT NULL,
|
||||||
|
"f1Negative" DOUBLE PRECISION NOT NULL,
|
||||||
|
"f1Neutral" DOUBLE PRECISION NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Model_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "UserPreference_userId_key" ON "UserPreference"("userId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Product_url_key" ON "Product"("url");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "UserPreference" ADD CONSTRAINT "UserPreference_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Review" ADD CONSTRAINT "Review_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Review" ADD CONSTRAINT "Review_modelId_fkey" FOREIGN KEY ("modelId") REFERENCES "Model"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Review" ADD CONSTRAINT "Review_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Analysis" ADD CONSTRAINT "Analysis_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Analysis" ADD CONSTRAINT "Analysis_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Analysis" ADD CONSTRAINT "Analysis_modelId_fkey" FOREIGN KEY ("modelId") REFERENCES "Model"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `preferedBrand` on the `UserPreference` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "UserPreference" DROP COLUMN "preferedBrand",
|
||||||
|
ADD COLUMN "preferredBrand" "Brand";
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Please do not edit this file manually
|
||||||
|
# It should be added in your version-control system (e.g., Git)
|
||||||
|
provider = "postgresql"
|
||||||
|
|
@ -40,6 +40,14 @@ enum Brand {
|
||||||
OTHER
|
OTHER
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum Profession {
|
||||||
|
PROGRAMMER
|
||||||
|
DESIGNER
|
||||||
|
STUDENT
|
||||||
|
GAMER
|
||||||
|
OTHER
|
||||||
|
}
|
||||||
|
|
||||||
model Account {
|
model Account {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
userId Int
|
userId Int
|
||||||
|
|
@ -96,9 +104,9 @@ model User {
|
||||||
|
|
||||||
model UserPreference {
|
model UserPreference {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
profession String?
|
profession Profession?
|
||||||
preferredOS OS?
|
preferredOS OS?
|
||||||
preferedBrand Brand?
|
preferredBrand Brand?
|
||||||
budgetMin Int?
|
budgetMin Int?
|
||||||
budgetMax Int?
|
budgetMax Int?
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
import { getServerSession } from "next-auth";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { authOptions } from "../auth/[...nextauth]/route";
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
|
||||||
|
if (!session?.user?.email) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Unauthorized. User belum login." },
|
||||||
|
{ status: 401 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await req.json();
|
||||||
|
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
bio,
|
||||||
|
profession,
|
||||||
|
preferredBrand,
|
||||||
|
preferredOS,
|
||||||
|
budgetMin,
|
||||||
|
budgetMax,
|
||||||
|
} = body;
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { email: session.user.email },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "User tidak ditemukan." },
|
||||||
|
{ status: 404 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updatedUser = await prisma.user.update({
|
||||||
|
where: { id: user.id },
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
bio,
|
||||||
|
preference: {
|
||||||
|
upsert: {
|
||||||
|
update: {
|
||||||
|
profession,
|
||||||
|
preferredBrand,
|
||||||
|
preferredOS,
|
||||||
|
budgetMin: budgetMin ? Number(budgetMin) : null,
|
||||||
|
budgetMax: budgetMax ? Number(budgetMax) : null,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
profession,
|
||||||
|
preferredBrand,
|
||||||
|
preferredOS,
|
||||||
|
budgetMin: budgetMin ? Number(budgetMin) : null,
|
||||||
|
budgetMax: budgetMax ? Number(budgetMax) : null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
preference: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: "Profile berhasil diupdate",
|
||||||
|
data: updatedUser,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Terjadi kesalahan server." },
|
||||||
|
{ status: 500 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
|
||||||
|
if (!session?.user?.email) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Unauthorized" },
|
||||||
|
{ status: 401 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { email: session.user.email },
|
||||||
|
include: {
|
||||||
|
preference: {
|
||||||
|
select: {
|
||||||
|
preferredBrand: true,
|
||||||
|
preferredOS: true,
|
||||||
|
profession: true,
|
||||||
|
budgetMax: true,
|
||||||
|
budgetMin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "User not found" },
|
||||||
|
{ status: 404 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(user);
|
||||||
|
}
|
||||||
|
|
@ -13,12 +13,13 @@ export const getAnotherUserData = async () => {
|
||||||
email: session.user.email,
|
email: session.user.email,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
|
name: true,
|
||||||
bio: true,
|
bio: true,
|
||||||
preference: {
|
preference: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
profession: true,
|
profession: true,
|
||||||
preferedBrand: true,
|
preferredBrand: true,
|
||||||
preferredOS: true,
|
preferredOS: true,
|
||||||
budgetMin: true,
|
budgetMin: true,
|
||||||
budgetMax: true,
|
budgetMax: true,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const brandEnum = z.enum([
|
||||||
|
"APPLE",
|
||||||
|
"ASUS",
|
||||||
|
"ACER",
|
||||||
|
"LENOVO",
|
||||||
|
"HP",
|
||||||
|
"DELL",
|
||||||
|
"MSI",
|
||||||
|
"AXIOO",
|
||||||
|
"ADVAN",
|
||||||
|
"ZYREX",
|
||||||
|
"OTHER",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const professionEnum = z.enum([
|
||||||
|
"PROGRAMMER",
|
||||||
|
"STUDENT",
|
||||||
|
"GAMER",
|
||||||
|
"DESIGNER",
|
||||||
|
"OTHER",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const osEnum = z.enum(["WINDOWS", "MACOS", "LINUX", "CHROME_OS", "OTHER"]);
|
||||||
|
|
||||||
|
export const profileSchema = z.object({
|
||||||
|
name: z.string().min(2, "Nama minimal 2 karakter"),
|
||||||
|
bio: z.string().min(10, "Bio minimal 10 karakter"),
|
||||||
|
profession: professionEnum,
|
||||||
|
preferredBrand: brandEnum,
|
||||||
|
preferredOS: osEnum,
|
||||||
|
budgetMin: z.coerce.number().min(0),
|
||||||
|
budgetMax: z.coerce.number().min(0),
|
||||||
|
});
|
||||||
|
|
@ -27,7 +27,7 @@ export function Header() {
|
||||||
|
|
||||||
if (!mounted) return null;
|
if (!mounted) return null;
|
||||||
return (
|
return (
|
||||||
<header className="border-b bg-[#F8FBFF]/50 backdrop-blur-sm sticky top-0 z-50">
|
<header className="border-b bg-[#F8FBFF]/50 backdrop-blur-sm sticky top-0 z-1">
|
||||||
<div className="container mx-auto px-4 py-4">
|
<div className="container mx-auto px-4 py-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Link href="/" className="flex items-center gap-3 cursor-pointer">
|
<Link href="/" className="flex items-center gap-3 cursor-pointer">
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,10 @@ export function ModelInfo({ data }: { data: ModelDB[] }) {
|
||||||
<SelectValue placeholder="Pilih Model" />
|
<SelectValue placeholder="Pilih Model" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|
||||||
<SelectContent className="bg-card border-border shadow-lg" position="popper">
|
<SelectContent
|
||||||
|
className="bg-card border-border shadow-lg"
|
||||||
|
position="popper"
|
||||||
|
>
|
||||||
{data.map((model, index) => (
|
{data.map((model, index) => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
key={model.modelName + index}
|
key={model.modelName + index}
|
||||||
|
|
@ -57,7 +60,7 @@ export function ModelInfo({ data }: { data: ModelDB[] }) {
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="mb-6 text-sm text-muted-foreground min-h-[40px]">
|
<p className="mb-6 text-sm text-muted-foreground min-h-10">
|
||||||
{currentModel.description}
|
{currentModel.description}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,53 +2,31 @@
|
||||||
|
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import {
|
import { Pencil, Wallet, Laptop, User, Monitor, Fan } from "lucide-react";
|
||||||
Pencil,
|
|
||||||
Wallet,
|
|
||||||
Laptop,
|
|
||||||
User,
|
|
||||||
Monitor,
|
|
||||||
Fan,
|
|
||||||
X,
|
|
||||||
Pickaxe,
|
|
||||||
Shell,
|
|
||||||
Save,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { ProfileClientProps } from "@/src/types";
|
import { ProfileClientProps } from "@/src/types";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { Separator } from "../ui/separator";
|
import { Separator } from "../ui/separator";
|
||||||
import { brandFormat, formatRupiah } from "@/src/utils/datas";
|
import { formatRupiah, toTitleCase } from "@/src/utils/datas";
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "../ui/select";
|
|
||||||
import { brandItems, OSItems, professionItems } from "@/src/utils/const";
|
import { brandItems, OSItems, professionItems } from "@/src/utils/const";
|
||||||
import { Input } from "../ui/input";
|
|
||||||
import { Label } from "../ui/label";
|
|
||||||
import { useProfileClient } from "@/src/hooks/useProfileClient";
|
import { useProfileClient } from "@/src/hooks/useProfileClient";
|
||||||
|
import { ProfileModal } from "./ProfileModal";
|
||||||
|
|
||||||
export default function ProfileCard({
|
export default function ProfileCard(props: ProfileClientProps) {
|
||||||
bio,
|
|
||||||
preferenceBrand,
|
|
||||||
preferenceOS,
|
|
||||||
budgetMax,
|
|
||||||
budgetMin,
|
|
||||||
}: ProfileClientProps) {
|
|
||||||
const { brands } = brandFormat({ preferenceBrand });
|
|
||||||
const {
|
const {
|
||||||
session,
|
session,
|
||||||
|
router,
|
||||||
showModal,
|
showModal,
|
||||||
|
name,
|
||||||
|
bio,
|
||||||
profession,
|
profession,
|
||||||
brand,
|
brands,
|
||||||
OS,
|
preferenceOS,
|
||||||
|
profileDatas,
|
||||||
|
budgetMin,
|
||||||
|
budgetMax,
|
||||||
setShowModal,
|
setShowModal,
|
||||||
setProfession,
|
handleOptimisticUpdate,
|
||||||
setBrand,
|
} = useProfileClient(props);
|
||||||
setOS,
|
|
||||||
} = useProfileClient();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|
@ -68,13 +46,16 @@ export default function ProfileCard({
|
||||||
height={88}
|
height={88}
|
||||||
className="h-20 w-20 rounded-full border-4 border-background object-cover shadow-sm"
|
className="h-20 w-20 rounded-full border-4 border-background object-cover shadow-sm"
|
||||||
/>
|
/>
|
||||||
<div className="absolute bottom-1 right-1 h-4 w-4 rounded-full border-2 border-background bg-sentiment-positive"></div>
|
<div className="absolute bottom-1 right-1 h-4 w-4 rounded-full border-2 border-background bg-green-500"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-card-foreground tracking-tight">
|
{name && (
|
||||||
{session?.data?.user?.name || "Guest User"}
|
<h1 className="text-2xl font-bold text-card-foreground tracking-tight">
|
||||||
</h1>
|
{/* {session?.data?.user?.name || "Guest User"} */}
|
||||||
|
{name || "Guest User"}
|
||||||
|
</h1>
|
||||||
|
)}
|
||||||
<p className="text-sm font-medium text-muted-foreground mb-2">
|
<p className="text-sm font-medium text-muted-foreground mb-2">
|
||||||
{session?.data?.user?.email || "Belum ada email"}
|
{session?.data?.user?.email || "Belum ada email"}
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -82,7 +63,7 @@ export default function ProfileCard({
|
||||||
{profession && (
|
{profession && (
|
||||||
<span className="inline-flex items-center gap-1.5 rounded-full bg-primary/10 px-2.5 py-0.5 text-xs font-semibold text-primary">
|
<span className="inline-flex items-center gap-1.5 rounded-full bg-primary/10 px-2.5 py-0.5 text-xs font-semibold text-primary">
|
||||||
<Fan className="w-3.5 h-3.5" />
|
<Fan className="w-3.5 h-3.5" />
|
||||||
<span className="capitalize">{profession}</span>
|
<span className="capitalize">{toTitleCase(profession)}</span>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -109,9 +90,8 @@ export default function ProfileCard({
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-xl bg-muted/50 p-4 border border-muted">
|
<div className="rounded-xl bg-muted/50 p-4 border border-muted">
|
||||||
<p className="text-sm leading-relaxed text-foreground/90">
|
<p className="text-sm leading-relaxed text-foreground/90">
|
||||||
{bio
|
{bio ||
|
||||||
? `${bio}`
|
"Belum ada deskripsi profil. Ceritakan sedikit tentang aktivitas dan kebutuhan laptop Anda."}
|
||||||
: "Belum ada deskripsi profil. Ceritakan sedikit tentang aktivitas dan kebutuhan laptop Anda."}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -174,11 +154,9 @@ export default function ProfileCard({
|
||||||
<p className="text-xl font-semibold">
|
<p className="text-xl font-semibold">
|
||||||
{formatRupiah(budgetMin)}
|
{formatRupiah(budgetMin)}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="my-2 h-px w-full bg-border"></div>
|
<div className="my-2 h-px w-full bg-border"></div>
|
||||||
|
|
||||||
<p className="text-sm text-muted-foreground mb-1">Hingga</p>
|
<p className="text-sm text-muted-foreground mb-1">Hingga</p>
|
||||||
<p className="text-xl font-semibold text-sentiment-positive">
|
<p className="text-xl font-semibold text-green-600">
|
||||||
{formatRupiah(budgetMax)}
|
{formatRupiah(budgetMax)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -194,206 +172,15 @@ export default function ProfileCard({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showModal && (
|
{showModal && (
|
||||||
<motion.div
|
<ProfileModal
|
||||||
initial={{ opacity: 0, y: 20 }}
|
setShowModal={setShowModal}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
professionItems={professionItems}
|
||||||
transition={{ duration: 0.2, ease: "circOut" }}
|
brandItems={brandItems}
|
||||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
|
OSItems={OSItems}
|
||||||
>
|
userData={profileDatas}
|
||||||
<form
|
onOptimisticUpdate={handleOptimisticUpdate}
|
||||||
action=""
|
router={router}
|
||||||
className=" flex flex-col bg-card w-1/3 p-6 rounded-2xl border relative"
|
/>
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-1 mb-4">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<User className="w-4 h-4" />
|
|
||||||
<Label htmlFor="username" className="font-semibold">
|
|
||||||
Nama Lengkap
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
<Input
|
|
||||||
id="username"
|
|
||||||
type="text"
|
|
||||||
placeholder="Masukkan nama lengkap Anda"
|
|
||||||
className="border rounded-md focus:ring-2 focus:ring-primary"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-1 mb-4">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Pickaxe className="w-4 h-4" />
|
|
||||||
<Label htmlFor="profession" className="font-semibold">
|
|
||||||
Profesi
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
<Select
|
|
||||||
name="profession"
|
|
||||||
value={profession}
|
|
||||||
onValueChange={setProfession}
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<SelectTrigger
|
|
||||||
className={`w-full ${!profession ? "text-gray-500" : "text-black"}`}
|
|
||||||
>
|
|
||||||
<SelectValue placeholder="Pilih Profesi/Kebutuhan" />
|
|
||||||
</SelectTrigger>
|
|
||||||
|
|
||||||
<SelectContent
|
|
||||||
className="bg-card border-border shadow-lg"
|
|
||||||
position="popper"
|
|
||||||
>
|
|
||||||
{professionItems.map((item) => {
|
|
||||||
const PIcon = item.icon;
|
|
||||||
return (
|
|
||||||
<SelectItem
|
|
||||||
key={item.value}
|
|
||||||
value={item.value}
|
|
||||||
className="cursor-pointer hover:bg-primary hover:text-card focus:bg-primary focus:text-card"
|
|
||||||
>
|
|
||||||
<div className="flex gap-2 items-center">
|
|
||||||
<span>
|
|
||||||
<PIcon className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</span>
|
|
||||||
<span>{item.label}</span>
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-1 mb-4">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Laptop className="w-4 h-4" />
|
|
||||||
<Label htmlFor="brand" className="font-semibold">
|
|
||||||
Merek Laptop
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
<Select
|
|
||||||
name="brand"
|
|
||||||
value={brand}
|
|
||||||
onValueChange={setBrand}
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<SelectTrigger
|
|
||||||
className={`w-full ${!brand ? "text-gray-500" : "text-black"}`}
|
|
||||||
>
|
|
||||||
<SelectValue placeholder="Pilih Merek Laptop" />
|
|
||||||
</SelectTrigger>
|
|
||||||
|
|
||||||
<SelectContent
|
|
||||||
className="bg-card border-border shadow-lg"
|
|
||||||
position="popper"
|
|
||||||
>
|
|
||||||
{brandItems.map((item) => {
|
|
||||||
const PIcon = item.icon;
|
|
||||||
return (
|
|
||||||
<SelectItem
|
|
||||||
key={item.value}
|
|
||||||
value={item.value}
|
|
||||||
className="cursor-pointer hover:bg-primary hover:text-card focus:bg-primary focus:text-card"
|
|
||||||
>
|
|
||||||
<div className="flex gap-2 items-center">
|
|
||||||
<span>
|
|
||||||
<PIcon className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</span>
|
|
||||||
<span>{item.label}</span>
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-2 mb-4">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Shell className="w-4 h-4" />
|
|
||||||
<Label htmlFor="OS" className="font-semibold">
|
|
||||||
Sistem Operasi
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
<Select name="OS" value={OS} onValueChange={setOS} required>
|
|
||||||
<SelectTrigger
|
|
||||||
className={`w-full -mt-1 ${!profession ? "text-gray-500" : "text-black"}`}
|
|
||||||
>
|
|
||||||
<SelectValue placeholder="Pilih Sistem Operasi" />
|
|
||||||
</SelectTrigger>
|
|
||||||
|
|
||||||
<SelectContent
|
|
||||||
className="bg-card border-border shadow-lg"
|
|
||||||
position="popper"
|
|
||||||
>
|
|
||||||
{OSItems.map((item) => {
|
|
||||||
const PIcon = item.icon;
|
|
||||||
return (
|
|
||||||
<SelectItem
|
|
||||||
key={item.value}
|
|
||||||
value={item.value}
|
|
||||||
className="cursor-pointer hover:bg-primary hover:text-card focus:bg-primary focus:text-card"
|
|
||||||
>
|
|
||||||
<div className="flex gap-2 items-center">
|
|
||||||
<span>
|
|
||||||
<PIcon className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</span>
|
|
||||||
<span>{item.label}</span>
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-1 mb-4">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Wallet className="w-4 h-4" />
|
|
||||||
<Label htmlFor="budget" className="font-semibold">
|
|
||||||
Rentang Anggaran
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-4">
|
|
||||||
<div className="flex-col w-1/2">
|
|
||||||
<Label htmlFor="budget" className="text-xs mt-2">
|
|
||||||
Rp (Minimal)
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="budget"
|
|
||||||
type="text"
|
|
||||||
placeholder="Rp 0"
|
|
||||||
className="border rounded-md focus:ring-2 focus:ring-primary mt-1"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex-col w-1/2">
|
|
||||||
<Label htmlFor="budget" className="text-xs mt-2">
|
|
||||||
Rp (Maksimal)
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="budget"
|
|
||||||
type="text"
|
|
||||||
placeholder="Rp 0"
|
|
||||||
className="border rounded-md focus:ring-2 focus:ring-primary mt-1"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-2 flex justify-start gap-4">
|
|
||||||
<Button onClick={() => setShowModal(false)} variant={"outline"}>
|
|
||||||
<X />
|
|
||||||
<span>Cancel</span>
|
|
||||||
</Button>
|
|
||||||
<Button type="submit">
|
|
||||||
<Save />
|
|
||||||
<span>Simpan</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
)}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { ArrowLeft } from "lucide-react";
|
import { ArrowLeft } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import ProfileCard from "./ProfileCard";
|
|
||||||
import { getAnotherUserData } from "@/src/app/profile/lib/action";
|
import { getAnotherUserData } from "@/src/app/profile/lib/action";
|
||||||
|
import ProfileCard from "./ProfileCard";
|
||||||
|
|
||||||
export default async function ProfileClient() {
|
export default async function ProfileClient() {
|
||||||
const user = await getAnotherUserData();
|
const user = await getAnotherUserData();
|
||||||
|
|
@ -19,12 +19,13 @@ export default async function ProfileClient() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ProfileCard
|
<ProfileCard
|
||||||
|
name={user?.name || ""}
|
||||||
|
profession={user?.preference?.profession || "OTHER"}
|
||||||
bio={user?.bio || "None"}
|
bio={user?.bio || "None"}
|
||||||
preferenceBrand={user?.preference?.preferedBrand || "None"}
|
preferenceBrand={user?.preference?.preferredBrand || "OTHER"}
|
||||||
preferenceOS={user?.preference?.preferredOS || "None"}
|
preferenceOS={user?.preference?.preferredOS || "OTHER"}
|
||||||
budgetMax={user?.preference?.budgetMax || 0}
|
budgetMax={user?.preference?.budgetMax || 0}
|
||||||
budgetMin={user?.preference?.budgetMin || 0}
|
budgetMin={user?.preference?.budgetMin || 0}
|
||||||
profession={user?.preference?.profession || "None"}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,280 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Controller } from "react-hook-form";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { User, Pickaxe, Laptop, Shell, Wallet, Save, X } from "lucide-react";
|
||||||
|
import { ExtendedModalProps } from "@/src/types";
|
||||||
|
import { Label } from "../ui/label";
|
||||||
|
import { Input } from "../ui/input";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "../ui/select";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { useProfileModal } from "@/src/hooks/useProfileModal";
|
||||||
|
|
||||||
|
export const ProfileModal = ({
|
||||||
|
setShowModal,
|
||||||
|
professionItems,
|
||||||
|
brandItems,
|
||||||
|
OSItems,
|
||||||
|
userData,
|
||||||
|
onOptimisticUpdate,
|
||||||
|
router,
|
||||||
|
}: ExtendedModalProps) => {
|
||||||
|
const { control, errors, isSubmitting, onSubmit, register, handleSubmit } =
|
||||||
|
useProfileModal({
|
||||||
|
userData,
|
||||||
|
router,
|
||||||
|
onOptimisticUpdate,
|
||||||
|
setShowModal,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.2, ease: "circOut" }}
|
||||||
|
className="fixed inset-0 flex items-center justify-center bg-black/50 z-1"
|
||||||
|
>
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
className="flex flex-col bg-card w-1/3 p-6 rounded-2xl border relative gap-4"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<User className="w-4 h-4" />
|
||||||
|
<Label htmlFor="name" className="font-semibold">
|
||||||
|
Nama Lengkap
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
placeholder="Masukkan nama lengkap Anda"
|
||||||
|
{...register("name")}
|
||||||
|
className="border rounded-md focus:ring-2 focus:ring-primary"
|
||||||
|
/>
|
||||||
|
{errors.name && (
|
||||||
|
<p className="text-red-500 text-xs">{errors.name.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<User className="w-4 h-4" />
|
||||||
|
<Label htmlFor="bio" className="font-semibold">
|
||||||
|
Bio
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
{...register("bio")}
|
||||||
|
placeholder="Masukkan bio Anda"
|
||||||
|
className="border rounded-md focus:ring-2 focus:ring-primary p-2 resize-none"
|
||||||
|
/>
|
||||||
|
{errors.bio && (
|
||||||
|
<p className="text-red-500 text-xs">{errors.bio.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Pickaxe className="w-4 h-4" />
|
||||||
|
<Label className="font-semibold">Profesi</Label>
|
||||||
|
</div>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="profession"
|
||||||
|
render={({ field }) => (
|
||||||
|
<Select
|
||||||
|
value={field.value || undefined}
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
className={`w-full ${!field.value ? "text-gray-500" : "text-black"}`}
|
||||||
|
>
|
||||||
|
<SelectValue placeholder="Pilih Profesi/Kebutuhan" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent
|
||||||
|
className="bg-card border-border shadow-lg"
|
||||||
|
position="popper"
|
||||||
|
>
|
||||||
|
{professionItems.map((item) => {
|
||||||
|
const PIcon = item.icon;
|
||||||
|
return (
|
||||||
|
<SelectItem
|
||||||
|
key={item.value}
|
||||||
|
value={item.value}
|
||||||
|
className="cursor-pointer hover:bg-primary hover:text-card focus:bg-primary focus:text-card"
|
||||||
|
>
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<PIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span>{item.label}</span>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{errors.profession && (
|
||||||
|
<p className="text-red-500 text-xs">{errors.profession.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Laptop className="w-4 h-4" />
|
||||||
|
<Label className="font-semibold">Merek Laptop</Label>
|
||||||
|
</div>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="preferredBrand"
|
||||||
|
render={({ field }) => (
|
||||||
|
<Select
|
||||||
|
value={field.value || undefined}
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
className={`w-full ${
|
||||||
|
!field.value ? "text-gray-500" : "text-black"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<SelectValue placeholder="Pilih Merek Laptop" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent
|
||||||
|
className="bg-card border-border shadow-lg"
|
||||||
|
position="popper"
|
||||||
|
>
|
||||||
|
{brandItems.map((item) => {
|
||||||
|
const PIcon = item.icon;
|
||||||
|
return (
|
||||||
|
<SelectItem
|
||||||
|
key={item.value}
|
||||||
|
value={item.value}
|
||||||
|
className="cursor-pointer hover:bg-primary hover:text-card focus:bg-primary focus:text-card"
|
||||||
|
>
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<PIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span>{item.label}</span>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{errors.preferredBrand && (
|
||||||
|
<p className="text-red-500 text-xs">
|
||||||
|
{errors.preferredBrand.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Shell className="w-4 h-4" />
|
||||||
|
<Label className="font-semibold">Sistem Operasi</Label>
|
||||||
|
</div>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="preferredOS"
|
||||||
|
render={({ field }) => (
|
||||||
|
<Select
|
||||||
|
value={field.value || undefined}
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
className={`w-full ${
|
||||||
|
!field.value ? "text-gray-500" : "text-black"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<SelectValue placeholder="Pilih Sistem Operasi" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent
|
||||||
|
className="bg-card border-border shadow-lg"
|
||||||
|
position="popper"
|
||||||
|
>
|
||||||
|
{OSItems.map((item) => {
|
||||||
|
const PIcon = item.icon;
|
||||||
|
return (
|
||||||
|
<SelectItem
|
||||||
|
key={item.value}
|
||||||
|
value={item.value}
|
||||||
|
className="cursor-pointer hover:bg-primary hover:text-card focus:bg-primary focus:text-card"
|
||||||
|
>
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<PIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span>{item.label}</span>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{errors.preferredOS && (
|
||||||
|
<p className="text-red-500 text-xs">{errors.preferredOS.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Wallet className="w-4 h-4" />
|
||||||
|
<Label className="font-semibold">Rentang Anggaran</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<div className="flex-col w-1/2">
|
||||||
|
<Label className="text-xs mt-2">Rp (Minimal)</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
{...register("budgetMin", { valueAsNumber: true })}
|
||||||
|
placeholder="Rp 0"
|
||||||
|
className="border rounded-md focus:ring-2 focus:ring-primary mt-1"
|
||||||
|
/>
|
||||||
|
{errors.budgetMin && (
|
||||||
|
<p className="text-red-500 text-xs">
|
||||||
|
{errors.budgetMin.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex-col w-1/2">
|
||||||
|
<Label className="text-xs mt-2">Rp (Maksimal)</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
{...register("budgetMax", { valueAsNumber: true })}
|
||||||
|
placeholder="Rp 0"
|
||||||
|
className="border rounded-md focus:ring-2 focus:ring-primary mt-1"
|
||||||
|
/>
|
||||||
|
{errors.budgetMax && (
|
||||||
|
<p className="text-red-500 text-xs">
|
||||||
|
{errors.budgetMax.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-2 flex justify-start gap-4">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowModal(false)}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
<X className="mr-2" />
|
||||||
|
<span>Cancel</span>
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={isSubmitting}>
|
||||||
|
<Save className="mr-2" />
|
||||||
|
<span>Simpan</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,24 +1,95 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { useState } from "react";
|
import { useRouter } from "next/navigation";
|
||||||
|
import { ProfileClientProps, ProfileFormData, ProfileState } from "@/src/types";
|
||||||
|
import { Brand, OS, Profession } from "@prisma/client";
|
||||||
|
import { brandFormat } from "../utils/datas";
|
||||||
|
|
||||||
export const useProfileClient = () => {
|
export const useProfileClient = (props: ProfileClientProps) => {
|
||||||
const session = useSession();
|
const session = useSession();
|
||||||
|
const router = useRouter();
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
const [profession, setProfession] = useState("");
|
|
||||||
const [brand, setBrand] = useState("");
|
const [profileDatas, setProfileDatas] = useState<ProfileState>({
|
||||||
const [OS, setOS] = useState("");
|
name: props.name || "",
|
||||||
|
bio: props.bio || "",
|
||||||
|
preference: {
|
||||||
|
profession: props.profession || "",
|
||||||
|
preferredBrand: props.preferenceBrand || "",
|
||||||
|
preferredOS: props.preferenceOS || "",
|
||||||
|
budgetMin: props.budgetMin ?? 0,
|
||||||
|
budgetMax: props.budgetMax ?? 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setProfileDatas({
|
||||||
|
name: props.name || "",
|
||||||
|
bio: props.bio || "",
|
||||||
|
preference: {
|
||||||
|
profession: props.profession || "",
|
||||||
|
preferredBrand: props.preferenceBrand || "",
|
||||||
|
preferredOS: props.preferenceOS || "",
|
||||||
|
budgetMin: props.budgetMin ?? 0,
|
||||||
|
budgetMax: props.budgetMax ?? 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [props]);
|
||||||
|
|
||||||
|
const handleOptimisticUpdate = (newData: ProfileFormData) => {
|
||||||
|
setProfileDatas((prev) => {
|
||||||
|
const updatedPreference = {
|
||||||
|
...prev.preference,
|
||||||
|
|
||||||
|
profession:
|
||||||
|
(newData.profession as Profession) || prev.preference.profession,
|
||||||
|
|
||||||
|
preferredBrand:
|
||||||
|
(newData.preferredBrand as Brand) || prev.preference.preferredBrand,
|
||||||
|
|
||||||
|
preferredOS: (newData.preferredOS as OS) || prev.preference.preferredOS,
|
||||||
|
|
||||||
|
budgetMin: newData.budgetMin ? Number(newData.budgetMin) : 0,
|
||||||
|
budgetMax: newData.budgetMax ? Number(newData.budgetMax) : 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
name: newData.name,
|
||||||
|
bio: newData.bio,
|
||||||
|
preference: updatedPreference,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const { name, bio, preference } = profileDatas;
|
||||||
|
|
||||||
|
const {
|
||||||
|
preferredBrand: preferenceBrand,
|
||||||
|
preferredOS: preferenceOS,
|
||||||
|
budgetMin,
|
||||||
|
budgetMax,
|
||||||
|
profession,
|
||||||
|
} = preference;
|
||||||
|
|
||||||
|
const { brands } = brandFormat({ preferenceBrand });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
session,
|
session,
|
||||||
|
router,
|
||||||
showModal,
|
showModal,
|
||||||
|
name,
|
||||||
|
bio,
|
||||||
profession,
|
profession,
|
||||||
brand,
|
brands,
|
||||||
OS,
|
preferenceBrand,
|
||||||
|
preferenceOS,
|
||||||
|
profileDatas,
|
||||||
|
budgetMin,
|
||||||
|
budgetMax,
|
||||||
|
handleOptimisticUpdate,
|
||||||
setShowModal,
|
setShowModal,
|
||||||
setProfession,
|
|
||||||
setBrand,
|
|
||||||
setOS,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { profileSchema } from "../app/validation/profile.schema";
|
||||||
|
import { updateProfileService } from "../services/profile.service";
|
||||||
|
import { ProfileFormData, UseProfileModalProps } from "../types";
|
||||||
|
|
||||||
|
export const useProfileModal = ({
|
||||||
|
userData,
|
||||||
|
router,
|
||||||
|
onOptimisticUpdate,
|
||||||
|
setShowModal,
|
||||||
|
}: UseProfileModalProps) => {
|
||||||
|
const pref = userData?.preference || {};
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
control,
|
||||||
|
formState: { errors, isSubmitting },
|
||||||
|
} = useForm<ProfileFormData>({
|
||||||
|
resolver: zodResolver(profileSchema),
|
||||||
|
defaultValues: {
|
||||||
|
name: userData?.name,
|
||||||
|
bio: userData?.bio,
|
||||||
|
profession: pref.profession ?? "OTHER",
|
||||||
|
preferredBrand: pref.preferredBrand ?? "OTHER",
|
||||||
|
preferredOS: pref.preferredOS ?? "OTHER",
|
||||||
|
budgetMin: pref.budgetMin ?? 0,
|
||||||
|
budgetMax: pref.budgetMax ?? 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (data: ProfileFormData) => {
|
||||||
|
setShowModal(false);
|
||||||
|
onOptimisticUpdate(data);
|
||||||
|
|
||||||
|
await updateProfileService(data)
|
||||||
|
.then(() => router.refresh())
|
||||||
|
.catch((err) => console.error("Update failed:", err));
|
||||||
|
};
|
||||||
|
|
||||||
|
return { register, handleSubmit, control, errors, isSubmitting, onSubmit };
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { ProfileFormData } from "../types";
|
||||||
|
|
||||||
|
export const updateProfileService = async (formData: ProfileFormData) => {
|
||||||
|
const response = await fetch("/api/profile", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(formData),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(result.message || "Failed to update profile");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import { LucideIcon } from "lucide-react";
|
import { LucideIcon } from "lucide-react";
|
||||||
import Brand, { Sentiment } from "@prisma/client";
|
import { OS, Profession, Sentiment, Brand } from "@prisma/client";
|
||||||
|
import z from "zod";
|
||||||
|
import { profileSchema } from "../app/validation/profile.schema";
|
||||||
|
|
||||||
export interface ModelDB {
|
export interface ModelDB {
|
||||||
modelName: string;
|
modelName: string;
|
||||||
|
|
@ -11,20 +13,21 @@ export interface ModelDB {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProfileClientProps {
|
export interface ProfileClientProps {
|
||||||
|
name: string;
|
||||||
bio?: string;
|
bio?: string;
|
||||||
preferenceBrand?: string;
|
preferenceBrand: Brand;
|
||||||
preferenceOS: string;
|
preferenceOS: OS;
|
||||||
budgetMin: number;
|
budgetMin: number;
|
||||||
budgetMax: number;
|
budgetMax: number;
|
||||||
profession: string;
|
profession: Profession;
|
||||||
id?: number;
|
id?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Brand {
|
// interface Brand {
|
||||||
name: string;
|
// name: string;
|
||||||
count: number;
|
// count: number;
|
||||||
logo?: string;
|
// logo?: string;
|
||||||
}
|
// }
|
||||||
|
|
||||||
export interface BrandFilterProps {
|
export interface BrandFilterProps {
|
||||||
// brands: Brand[];
|
// brands: Brand[];
|
||||||
|
|
@ -202,6 +205,52 @@ export interface ResultProps {
|
||||||
result: AnalysisResults | null;
|
result: AnalysisResults | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ProfileFormData = z.input<typeof profileSchema>;
|
||||||
|
|
||||||
|
export type ProfileModalProps = {
|
||||||
|
setShowModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
professionItems: { value: string; label: string; icon: any }[];
|
||||||
|
brandItems: { value: string; label: string; icon: any }[];
|
||||||
|
OSItems: { value: string; label: string; icon: any }[];
|
||||||
|
};
|
||||||
|
|
||||||
export interface WordCLoud {
|
export interface WordCLoud {
|
||||||
topKeywords: string;
|
topKeywords: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ModalProps extends ProfileModalProps {
|
||||||
|
userData: {
|
||||||
|
name: string;
|
||||||
|
bio: string;
|
||||||
|
preference: {
|
||||||
|
profession: Profession;
|
||||||
|
preferredBrand: Brand;
|
||||||
|
preferredOS: OS;
|
||||||
|
budgetMin: number;
|
||||||
|
budgetMax: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExtendedModalProps extends ProfileModalProps {
|
||||||
|
userData: any;
|
||||||
|
onOptimisticUpdate: (data: ProfileFormData) => void;
|
||||||
|
router: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProfileState {
|
||||||
|
name: string;
|
||||||
|
bio: string;
|
||||||
|
preference: {
|
||||||
|
profession: Profession | string;
|
||||||
|
preferredBrand: Brand | string;
|
||||||
|
preferredOS: OS | string;
|
||||||
|
budgetMin: number;
|
||||||
|
budgetMax: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UseProfileModalProps = Pick<
|
||||||
|
ExtendedModalProps,
|
||||||
|
"userData" | "router" | "onOptimisticUpdate" | "setShowModal"
|
||||||
|
>;
|
||||||
|
|
|
||||||
|
|
@ -29,22 +29,22 @@ export const MODEL_OPTIONS = [
|
||||||
export const WORD_LIMIT = 15;
|
export const WORD_LIMIT = 15;
|
||||||
|
|
||||||
export const professionItems = [
|
export const professionItems = [
|
||||||
{ value: "programmer", label: "Programmer", icon: Code },
|
{ value: "PROGRAMMER", label: "Programmer", icon: Code },
|
||||||
{ value: "designer", label: "Designer", icon: Palette },
|
{ value: "DESIGNER", label: "Designer", icon: Palette },
|
||||||
{ value: "student", label: "Student", icon: Book },
|
{ value: "STUDENT", label: "Student", icon: Book },
|
||||||
{ value: "gamer", label: "Gamer", icon: GamepadDirectional },
|
{ value: "GAMER", label: "Gamer", icon: GamepadDirectional },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const brandItems = [
|
export const brandItems = [
|
||||||
{ value: "asus", label: "Asus", icon: SiAsus },
|
{ value: "ASUS", label: "Asus", icon: SiAsus },
|
||||||
{ value: "acer", label: "Acer", icon: SiAcer },
|
{ value: "ACER", label: "Acer", icon: SiAcer },
|
||||||
{ value: "lenovo", label: "Lenovo", icon: SiLenovo },
|
{ value: "LENOVO", label: "Lenovo", icon: SiLenovo },
|
||||||
{ value: "other", label: "Other", icon: LucideCircleEllipsis },
|
{ value: "OTHER", label: "Other", icon: LucideCircleEllipsis },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const OSItems = [
|
export const OSItems = [
|
||||||
{ value: "windows", label: "Windows", icon: FaWindows },
|
{ value: "WINDOWS", label: "Windows", icon: FaWindows },
|
||||||
{ value: "macos", label: "Macos", icon: SiMacos },
|
{ value: "MACOS", label: "Macos", icon: SiMacos },
|
||||||
{ value: "linux", label: "Linux", icon: SiLinux },
|
{ value: "LINUX", label: "Linux", icon: SiLinux },
|
||||||
{ value: "other", label: "Other", icon: LucideCircleEllipsis },
|
{ value: "OTHER", label: "Other", icon: LucideCircleEllipsis },
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,6 @@
|
||||||
import { Frown, Meh, Smile } from "lucide-react";
|
import { Frown, Meh, Smile } from "lucide-react";
|
||||||
import {
|
import { ScrapeResult, WordCloudConfig, WordItem } from "../types";
|
||||||
ProfileClientProps,
|
import { Brand } from "@prisma/client";
|
||||||
ScrapeResult,
|
|
||||||
WordCloudConfig,
|
|
||||||
WordItem,
|
|
||||||
} from "../types";
|
|
||||||
|
|
||||||
export const getSentimentDisplay = (sentiment: string) => {
|
export const getSentimentDisplay = (sentiment: string) => {
|
||||||
switch (sentiment?.toLowerCase()) {
|
switch (sentiment?.toLowerCase()) {
|
||||||
|
|
@ -85,13 +81,23 @@ export const formatRupiah = (value: number | string) => {
|
||||||
}).format(Number(value));
|
}).format(Number(value));
|
||||||
};
|
};
|
||||||
|
|
||||||
export function brandFormat({
|
export const brandFormat = ({
|
||||||
preferenceBrand,
|
preferenceBrand,
|
||||||
}: Pick<ProfileClientProps, "preferenceBrand">) {
|
}: {
|
||||||
|
preferenceBrand: Brand | string;
|
||||||
|
}) => {
|
||||||
const brands = Array.isArray(preferenceBrand)
|
const brands = Array.isArray(preferenceBrand)
|
||||||
? preferenceBrand
|
? preferenceBrand
|
||||||
: preferenceBrand
|
: preferenceBrand
|
||||||
? [preferenceBrand]
|
? [preferenceBrand]
|
||||||
: [];
|
: [];
|
||||||
return { brands };
|
return { brands };
|
||||||
|
};
|
||||||
|
|
||||||
|
export function toTitleCase(str: string) {
|
||||||
|
return str
|
||||||
|
.toLowerCase()
|
||||||
|
.split(/[\s-_]+/)
|
||||||
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||||
|
.join(" ");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue