Add CommissionTypeForm
This commit is contained in:
130
package-lock.json
generated
130
package-lock.json
generated
@ -9,6 +9,8 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
"@dnd-kit/modifiers": "^9.0.0",
|
||||||
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@hookform/resolvers": "^5.1.1",
|
"@hookform/resolvers": "^5.1.1",
|
||||||
"@prisma/client": "^6.11.1",
|
"@prisma/client": "^6.11.1",
|
||||||
@ -17,12 +19,15 @@
|
|||||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
"@radix-ui/react-navigation-menu": "^1.2.13",
|
"@radix-ui/react-navigation-menu": "^1.2.13",
|
||||||
|
"@radix-ui/react-popover": "^1.1.14",
|
||||||
"@radix-ui/react-progress": "^1.1.7",
|
"@radix-ui/react-progress": "^1.1.7",
|
||||||
"@radix-ui/react-select": "^2.2.5",
|
"@radix-ui/react-select": "^2.2.5",
|
||||||
|
"@radix-ui/react-slider": "^1.3.5",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-switch": "^1.2.5",
|
"@radix-ui/react-switch": "^1.2.5",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "^1.1.1",
|
||||||
"lucide-react": "^0.525.0",
|
"lucide-react": "^0.525.0",
|
||||||
"next": "15.3.5",
|
"next": "15.3.5",
|
||||||
"next-auth": "^5.0.0-beta.29",
|
"next-auth": "^5.0.0-beta.29",
|
||||||
@ -30,6 +35,7 @@
|
|||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-hook-form": "^7.59.0",
|
"react-hook-form": "^7.59.0",
|
||||||
|
"sonner": "^2.0.6",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"zod": "^3.25.73"
|
"zod": "^3.25.73"
|
||||||
},
|
},
|
||||||
@ -132,6 +138,34 @@
|
|||||||
"react-dom": ">=16.8.0"
|
"react-dom": ">=16.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@dnd-kit/modifiers": {
|
||||||
|
"version": "9.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-9.0.0.tgz",
|
||||||
|
"integrity": "sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@dnd-kit/core": "^6.3.0",
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit/sortable": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@dnd-kit/core": "^6.3.0",
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@dnd-kit/utilities": {
|
"node_modules/@dnd-kit/utilities": {
|
||||||
"version": "3.2.2",
|
"version": "3.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
|
||||||
@ -1589,6 +1623,43 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-popover": {
|
||||||
|
"version": "1.1.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.14.tgz",
|
||||||
|
"integrity": "sha512-ODz16+1iIbGUfFEfKx2HTPKizg2MN39uIOV8MXeHnmdd3i/N9Wt7vU46wbHsqA0xoaQyXVcs0KIlBdOA2Y95bw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.2",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-dismissable-layer": "1.1.10",
|
||||||
|
"@radix-ui/react-focus-guards": "1.1.2",
|
||||||
|
"@radix-ui/react-focus-scope": "1.1.7",
|
||||||
|
"@radix-ui/react-id": "1.1.1",
|
||||||
|
"@radix-ui/react-popper": "1.2.7",
|
||||||
|
"@radix-ui/react-portal": "1.1.9",
|
||||||
|
"@radix-ui/react-presence": "1.1.4",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-slot": "1.2.3",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||||
|
"aria-hidden": "^1.2.4",
|
||||||
|
"react-remove-scroll": "^2.6.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-popper": {
|
"node_modules/@radix-ui/react-popper": {
|
||||||
"version": "1.2.7",
|
"version": "1.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz",
|
||||||
@ -1790,6 +1861,39 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-slider": {
|
||||||
|
"version": "1.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.5.tgz",
|
||||||
|
"integrity": "sha512-rkfe2pU2NBAYfGaxa3Mqosi7VZEWX5CxKaanRv0vZd4Zhl9fvQrg0VM93dv3xGLGfrHuoTRF3JXH8nb9g+B3fw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/number": "1.1.1",
|
||||||
|
"@radix-ui/primitive": "1.1.2",
|
||||||
|
"@radix-ui/react-collection": "1.1.7",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-direction": "1.1.1",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1",
|
||||||
|
"@radix-ui/react-use-previous": "1.1.1",
|
||||||
|
"@radix-ui/react-use-size": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-slot": {
|
"node_modules/@radix-ui/react-slot": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||||
@ -3405,6 +3509,22 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cmdk": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "^1.1.1",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.6",
|
||||||
|
"@radix-ui/react-id": "^1.1.0",
|
||||||
|
"@radix-ui/react-primitive": "^2.0.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18 || ^19 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^18 || ^19 || ^19.0.0-rc"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color": {
|
"node_modules/color": {
|
||||||
"version": "4.2.3",
|
"version": "4.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
||||||
@ -6876,6 +6996,16 @@
|
|||||||
"is-arrayish": "^0.3.1"
|
"is-arrayish": "^0.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sonner": {
|
||||||
|
"version": "2.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.6.tgz",
|
||||||
|
"integrity": "sha512-yHFhk8T/DK3YxjFQXIrcHT1rGEeTLliVzWbO0xN8GberVun2RiBnxAjXAYpZrqwEVHBG9asI/Li8TAAhN9m59Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
|
@ -10,6 +10,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
"@dnd-kit/modifiers": "^9.0.0",
|
||||||
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@hookform/resolvers": "^5.1.1",
|
"@hookform/resolvers": "^5.1.1",
|
||||||
"@prisma/client": "^6.11.1",
|
"@prisma/client": "^6.11.1",
|
||||||
@ -18,12 +20,15 @@
|
|||||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
"@radix-ui/react-navigation-menu": "^1.2.13",
|
"@radix-ui/react-navigation-menu": "^1.2.13",
|
||||||
|
"@radix-ui/react-popover": "^1.1.14",
|
||||||
"@radix-ui/react-progress": "^1.1.7",
|
"@radix-ui/react-progress": "^1.1.7",
|
||||||
"@radix-ui/react-select": "^2.2.5",
|
"@radix-ui/react-select": "^2.2.5",
|
||||||
|
"@radix-ui/react-slider": "^1.3.5",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-switch": "^1.2.5",
|
"@radix-ui/react-switch": "^1.2.5",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "^1.1.1",
|
||||||
"lucide-react": "^0.525.0",
|
"lucide-react": "^0.525.0",
|
||||||
"next": "15.3.5",
|
"next": "15.3.5",
|
||||||
"next-auth": "^5.0.0-beta.29",
|
"next-auth": "^5.0.0-beta.29",
|
||||||
@ -31,6 +36,7 @@
|
|||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-hook-form": "^7.59.0",
|
"react-hook-form": "^7.59.0",
|
||||||
|
"sonner": "^2.0.6",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"zod": "^3.25.73"
|
"zod": "^3.25.73"
|
||||||
},
|
},
|
||||||
|
127
prisma/migrations/20250705193450_commision_types/migration.sql
Normal file
127
prisma/migrations/20250705193450_commision_types/migration.sql
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `active` on the `CommissionType` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `basePrice` on the `CommissionType` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `deliveryEst` on the `CommissionType` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `tags` on the `CommissionType` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `title` on the `CommissionType` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the `Artwork` table. If the table is not empty, all the data it contains will be lost.
|
||||||
|
- You are about to drop the `CommissionRequest` table. If the table is not empty, all the data it contains will be lost.
|
||||||
|
- You are about to drop the `Preferences` table. If the table is not empty, all the data it contains will be lost.
|
||||||
|
- You are about to drop the `PresentationGroup` table. If the table is not empty, all the data it contains will be lost.
|
||||||
|
- You are about to drop the `TOS` table. If the table is not empty, all the data it contains will be lost.
|
||||||
|
- You are about to drop the `User` table. If the table is not empty, all the data it contains will be lost.
|
||||||
|
- Added the required column `name` to the `CommissionType` table without a default value. This is not possible if the table is not empty.
|
||||||
|
- Added the required column `updatedAt` to the `CommissionType` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "Artwork" DROP CONSTRAINT "Artwork_groupId_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "CommissionRequest" DROP CONSTRAINT "CommissionRequest_typeId_fkey";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "CommissionType" DROP COLUMN "active",
|
||||||
|
DROP COLUMN "basePrice",
|
||||||
|
DROP COLUMN "deliveryEst",
|
||||||
|
DROP COLUMN "tags",
|
||||||
|
DROP COLUMN "title",
|
||||||
|
ADD COLUMN "name" TEXT NOT NULL,
|
||||||
|
ADD COLUMN "sortIndex" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL;
|
||||||
|
|
||||||
|
-- DropTable
|
||||||
|
DROP TABLE "Artwork";
|
||||||
|
|
||||||
|
-- DropTable
|
||||||
|
DROP TABLE "CommissionRequest";
|
||||||
|
|
||||||
|
-- DropTable
|
||||||
|
DROP TABLE "Preferences";
|
||||||
|
|
||||||
|
-- DropTable
|
||||||
|
DROP TABLE "PresentationGroup";
|
||||||
|
|
||||||
|
-- DropTable
|
||||||
|
DROP TABLE "TOS";
|
||||||
|
|
||||||
|
-- DropTable
|
||||||
|
DROP TABLE "User";
|
||||||
|
|
||||||
|
-- DropEnum
|
||||||
|
DROP TYPE "RequestStatus";
|
||||||
|
|
||||||
|
-- DropEnum
|
||||||
|
DROP TYPE "Role";
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "CommissionOption" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"sortIndex" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"description" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "CommissionOption_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "CommissionExtra" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"sortIndex" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"description" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "CommissionExtra_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "CommissionTypeOption" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"typeId" TEXT NOT NULL,
|
||||||
|
"optionId" TEXT NOT NULL,
|
||||||
|
"priceRange" TEXT,
|
||||||
|
"pricePercent" DOUBLE PRECISION,
|
||||||
|
"price" DOUBLE PRECISION,
|
||||||
|
|
||||||
|
CONSTRAINT "CommissionTypeOption_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "CommissionExtraOption" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"typeId" TEXT NOT NULL,
|
||||||
|
"extraId" TEXT NOT NULL,
|
||||||
|
"priceRange" TEXT,
|
||||||
|
"pricePercent" DOUBLE PRECISION,
|
||||||
|
"price" DOUBLE PRECISION,
|
||||||
|
|
||||||
|
CONSTRAINT "CommissionExtraOption_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "CommissionTypeOption_typeId_optionId_key" ON "CommissionTypeOption"("typeId", "optionId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "CommissionExtraOption_typeId_extraId_key" ON "CommissionExtraOption"("typeId", "extraId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "CommissionTypeOption" ADD CONSTRAINT "CommissionTypeOption_typeId_fkey" FOREIGN KEY ("typeId") REFERENCES "CommissionType"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "CommissionTypeOption" ADD CONSTRAINT "CommissionTypeOption_optionId_fkey" FOREIGN KEY ("optionId") REFERENCES "CommissionOption"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "CommissionExtraOption" ADD CONSTRAINT "CommissionExtraOption_typeId_fkey" FOREIGN KEY ("typeId") REFERENCES "CommissionType"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "CommissionExtraOption" ADD CONSTRAINT "CommissionExtraOption_extraId_fkey" FOREIGN KEY ("extraId") REFERENCES "CommissionExtra"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the `CommissionExtraOption` table. If the table is not empty, all the data it contains will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "CommissionExtraOption" DROP CONSTRAINT "CommissionExtraOption_extraId_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "CommissionExtraOption" DROP CONSTRAINT "CommissionExtraOption_typeId_fkey";
|
||||||
|
|
||||||
|
-- DropTable
|
||||||
|
DROP TABLE "CommissionExtraOption";
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "CommissionTypeExtra" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"typeId" TEXT NOT NULL,
|
||||||
|
"extraId" TEXT NOT NULL,
|
||||||
|
"priceRange" TEXT,
|
||||||
|
"pricePercent" DOUBLE PRECISION,
|
||||||
|
"price" DOUBLE PRECISION,
|
||||||
|
|
||||||
|
CONSTRAINT "CommissionTypeExtra_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "CommissionTypeExtra_typeId_extraId_key" ON "CommissionTypeExtra"("typeId", "extraId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "CommissionTypeExtra" ADD CONSTRAINT "CommissionTypeExtra_typeId_fkey" FOREIGN KEY ("typeId") REFERENCES "CommissionType"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "CommissionTypeExtra" ADD CONSTRAINT "CommissionTypeExtra_extraId_fkey" FOREIGN KEY ("extraId") REFERENCES "CommissionExtra"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
@ -0,0 +1,5 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "CommissionTypeExtra" ADD COLUMN "sortIndex" INTEGER NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "CommissionTypeOption" ADD COLUMN "sortIndex" INTEGER NOT NULL DEFAULT 0;
|
@ -14,83 +14,161 @@ datasource db {
|
|||||||
url = env("DATABASE_URL")
|
url = env("DATABASE_URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
|
||||||
id String @id @default(cuid())
|
|
||||||
email String @unique
|
|
||||||
name String?
|
|
||||||
role Role @default(ADMIN)
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Role {
|
|
||||||
ADMIN
|
|
||||||
ARTIST
|
|
||||||
}
|
|
||||||
|
|
||||||
model CommissionType {
|
model CommissionType {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
title String
|
|
||||||
description String?
|
|
||||||
basePrice Float
|
|
||||||
deliveryEst String? // e.g. "2 weeks"
|
|
||||||
tags String[] // e.g. shaded, sketch, full-body
|
|
||||||
active Boolean @default(true)
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
CommissionRequest CommissionRequest[]
|
updatedAt DateTime @updatedAt
|
||||||
|
sortIndex Int @default(0)
|
||||||
|
|
||||||
|
name String
|
||||||
|
|
||||||
|
description String?
|
||||||
|
|
||||||
|
options CommissionTypeOption[]
|
||||||
|
extras CommissionTypeExtra[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model CommissionRequest {
|
model CommissionOption {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String
|
|
||||||
email String
|
|
||||||
message String
|
|
||||||
typeId String
|
|
||||||
status RequestStatus @default(PENDING)
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
sortIndex Int @default(0)
|
||||||
|
|
||||||
|
name String
|
||||||
|
|
||||||
|
description String?
|
||||||
|
|
||||||
|
types CommissionTypeOption[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model CommissionExtra {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
sortIndex Int @default(0)
|
||||||
|
|
||||||
|
name String
|
||||||
|
|
||||||
|
description String?
|
||||||
|
|
||||||
|
types CommissionTypeExtra[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model CommissionTypeOption {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
sortIndex Int @default(0)
|
||||||
|
|
||||||
|
typeId String
|
||||||
|
optionId String
|
||||||
|
|
||||||
|
priceRange String?
|
||||||
|
pricePercent Float?
|
||||||
|
price Float?
|
||||||
|
|
||||||
type CommissionType @relation(fields: [typeId], references: [id])
|
type CommissionType @relation(fields: [typeId], references: [id])
|
||||||
|
option CommissionOption @relation(fields: [optionId], references: [id])
|
||||||
|
|
||||||
|
@@unique([typeId, optionId])
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RequestStatus {
|
model CommissionTypeExtra {
|
||||||
PENDING
|
|
||||||
ACCEPTED
|
|
||||||
IN_PROGRESS
|
|
||||||
DONE
|
|
||||||
REJECTED
|
|
||||||
}
|
|
||||||
|
|
||||||
model Artwork {
|
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
title String
|
|
||||||
imageUrl String
|
|
||||||
description String?
|
|
||||||
tags String[]
|
|
||||||
formats String[]
|
|
||||||
isPublic Boolean @default(true)
|
|
||||||
groupId String?
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
sortIndex Int @default(0)
|
||||||
|
|
||||||
group PresentationGroup? @relation(fields: [groupId], references: [id])
|
typeId String
|
||||||
|
extraId String
|
||||||
|
|
||||||
|
priceRange String?
|
||||||
|
pricePercent Float?
|
||||||
|
price Float?
|
||||||
|
|
||||||
|
type CommissionType @relation(fields: [typeId], references: [id])
|
||||||
|
extra CommissionExtra @relation(fields: [extraId], references: [id])
|
||||||
|
|
||||||
|
@@unique([typeId, extraId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model PresentationGroup {
|
// model User {
|
||||||
id String @id @default(cuid())
|
// id String @id @default(cuid())
|
||||||
name String
|
// email String @unique
|
||||||
description String?
|
// name String?
|
||||||
createdAt DateTime @default(now())
|
// role Role @default(ADMIN)
|
||||||
Artwork Artwork[]
|
// createdAt DateTime @default(now())
|
||||||
}
|
// }
|
||||||
|
|
||||||
model Preferences {
|
// enum Role {
|
||||||
id String @id @default(cuid())
|
// ADMIN
|
||||||
commissionOpen Boolean @default(true)
|
// ARTIST
|
||||||
defaultDelivery String? // e.g. "7 days"
|
// }
|
||||||
autoReplyMessage String?
|
|
||||||
notifyByEmail Boolean @default(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
model TOS {
|
// model CommissionType {
|
||||||
id String @id @default(cuid())
|
// id String @id @default(cuid())
|
||||||
content String // Markdown or rich text
|
// title String
|
||||||
createdAt DateTime @default(now())
|
// description String?
|
||||||
}
|
// basePrice Float
|
||||||
|
// deliveryEst String? // e.g. "2 weeks"
|
||||||
|
// tags String[] // e.g. shaded, sketch, full-body
|
||||||
|
// active Boolean @default(true)
|
||||||
|
// createdAt DateTime @default(now())
|
||||||
|
// CommissionRequest CommissionRequest[]
|
||||||
|
// }
|
||||||
|
|
||||||
|
// model CommissionRequest {
|
||||||
|
// id String @id @default(cuid())
|
||||||
|
// name String
|
||||||
|
// email String
|
||||||
|
// message String
|
||||||
|
// typeId String
|
||||||
|
// status RequestStatus @default(PENDING)
|
||||||
|
// createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
// type CommissionType @relation(fields: [typeId], references: [id])
|
||||||
|
// }
|
||||||
|
|
||||||
|
// enum RequestStatus {
|
||||||
|
// PENDING
|
||||||
|
// ACCEPTED
|
||||||
|
// IN_PROGRESS
|
||||||
|
// DONE
|
||||||
|
// REJECTED
|
||||||
|
// }
|
||||||
|
|
||||||
|
// model Artwork {
|
||||||
|
// id String @id @default(cuid())
|
||||||
|
// title String
|
||||||
|
// imageUrl String
|
||||||
|
// description String?
|
||||||
|
// tags String[]
|
||||||
|
// formats String[]
|
||||||
|
// isPublic Boolean @default(true)
|
||||||
|
// groupId String?
|
||||||
|
// createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
// group PresentationGroup? @relation(fields: [groupId], references: [id])
|
||||||
|
// }
|
||||||
|
|
||||||
|
// model PresentationGroup {
|
||||||
|
// id String @id @default(cuid())
|
||||||
|
// name String
|
||||||
|
// description String?
|
||||||
|
// createdAt DateTime @default(now())
|
||||||
|
// Artwork Artwork[]
|
||||||
|
// }
|
||||||
|
|
||||||
|
// model Preferences {
|
||||||
|
// id String @id @default(cuid())
|
||||||
|
// commissionOpen Boolean @default(true)
|
||||||
|
// defaultDelivery String? // e.g. "7 days"
|
||||||
|
// autoReplyMessage String?
|
||||||
|
// notifyByEmail Boolean @default(true)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// model TOS {
|
||||||
|
// id String @id @default(cuid())
|
||||||
|
// content String // Markdown or rich text
|
||||||
|
// createdAt DateTime @default(now())
|
||||||
|
// }
|
||||||
|
58
src/actions/items/commissions/types/newType.ts
Normal file
58
src/actions/items/commissions/types/newType.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
"use server"
|
||||||
|
|
||||||
|
import prisma from "@/lib/prisma"
|
||||||
|
import { commissionTypeSchema } from "@/schemas/commissionType"
|
||||||
|
|
||||||
|
export async function createCommissionOption(data: { name: string }) {
|
||||||
|
return await prisma.commissionOption.create({
|
||||||
|
data: {
|
||||||
|
name: data.name,
|
||||||
|
description: "",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createCommissionExtra(data: { name: string }) {
|
||||||
|
return await prisma.commissionExtra.create({
|
||||||
|
data: {
|
||||||
|
name: data.name,
|
||||||
|
description: "",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createCommissionType(formData: commissionTypeSchema) {
|
||||||
|
const parsed = commissionTypeSchema.safeParse(formData)
|
||||||
|
|
||||||
|
if (!parsed.success) {
|
||||||
|
console.error("Validation failed", parsed.error)
|
||||||
|
throw new Error("Invalid input")
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = parsed.data
|
||||||
|
|
||||||
|
const created = await prisma.commissionType.create({
|
||||||
|
data: {
|
||||||
|
name: data.name,
|
||||||
|
description: data.description,
|
||||||
|
options: {
|
||||||
|
create: data.options?.map((opt) => ({
|
||||||
|
option: { connect: { id: opt.optionId } },
|
||||||
|
price: opt.price,
|
||||||
|
pricePercent: opt.pricePercent,
|
||||||
|
priceRange: opt.priceRange,
|
||||||
|
})) || [],
|
||||||
|
},
|
||||||
|
extras: {
|
||||||
|
create: data.extras?.map((ext) => ({
|
||||||
|
extra: { connect: { id: ext.extraId } },
|
||||||
|
price: ext.price,
|
||||||
|
pricePercent: ext.pricePercent,
|
||||||
|
priceRange: ext.priceRange,
|
||||||
|
})) || [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return created
|
||||||
|
}
|
21
src/app/items/commissions/types/new/page.tsx
Normal file
21
src/app/items/commissions/types/new/page.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import NewTypeForm from "@/components/items/commissions/types/NewTypeForm";
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
|
||||||
|
export default async function CommissionTypesNewPage() {
|
||||||
|
const options = await prisma.commissionOption.findMany({
|
||||||
|
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
|
||||||
|
});
|
||||||
|
const extras = await prisma.commissionExtra.findMany({
|
||||||
|
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex gap-4 justify-between pb-8">
|
||||||
|
<h1 className="text-2xl font-bold mb-4">New Commission Type</h1>
|
||||||
|
</div>
|
||||||
|
<NewTypeForm options={options} extras={extras} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
26
src/app/items/commissions/types/page.tsx
Normal file
26
src/app/items/commissions/types/page.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import ListTypes from "@/components/items/commissions/types/ListTypes";
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { PlusCircleIcon } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export default async function CommissionTypesPage() {
|
||||||
|
const types = await prisma.commissionType.findMany({
|
||||||
|
include: {
|
||||||
|
options: { include: { option: true } },
|
||||||
|
extras: { include: { extra: true } },
|
||||||
|
},
|
||||||
|
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex gap-4 justify-between pb-8">
|
||||||
|
<h1 className="text-2xl font-bold mb-4">Commission Types</h1>
|
||||||
|
<Link href="/items/commissions/types/new" className="flex gap-2 items-center cursor-pointer bg-primary hover:bg-primary/90 text-primary-foreground px-4 py-2 rounded">
|
||||||
|
<PlusCircleIcon className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all text-primary-foreground" /> Add new Type
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
{types && types.length > 0 ? <ListTypes types={types} /> : <p className="text-muted-foreground italic">No types found.</p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
import Footer from "@/components/global/Footer";
|
import Footer from "@/components/global/Footer";
|
||||||
import Header from "@/components/global/Header";
|
import Header from "@/components/global/Header";
|
||||||
import { ThemeProvider } from "@/components/global/ThemeProvider";
|
import { ThemeProvider } from "@/components/global/ThemeProvider";
|
||||||
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
@ -65,6 +66,7 @@ export default function RootLayout({
|
|||||||
<Footer />
|
<Footer />
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
<Toaster />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
64
src/components/items/commissions/types/ListTypes.tsx
Normal file
64
src/components/items/commissions/types/ListTypes.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { CommissionExtra, CommissionOption, CommissionType, CommissionTypeExtra, CommissionTypeOption } from "@/generated/prisma";
|
||||||
|
|
||||||
|
type CommissionTypeWithItems = CommissionType & {
|
||||||
|
options: (CommissionTypeOption & {
|
||||||
|
option: CommissionOption | null
|
||||||
|
})[]
|
||||||
|
extras: (CommissionTypeExtra & {
|
||||||
|
extra: CommissionExtra | null
|
||||||
|
})[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ListTypes({ types }: { types: CommissionTypeWithItems[] }) {
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
{types.map(type => (
|
||||||
|
<Card key={type.id}>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-xl truncate">{type.name}</CardTitle>
|
||||||
|
<CardDescription>{type.description}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex flex-col justify-start gap-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold">Options</h4>
|
||||||
|
<ul className="pl-4 list-disc">
|
||||||
|
{type.options.map((opt) => (
|
||||||
|
<li key={opt.id}>
|
||||||
|
{opt.option?.name}:{" "}
|
||||||
|
{opt.price !== null
|
||||||
|
? `${opt.price}€`
|
||||||
|
: opt.pricePercent
|
||||||
|
? `+${opt.pricePercent}%`
|
||||||
|
: opt.priceRange
|
||||||
|
? `${opt.priceRange}€`
|
||||||
|
: "Included"}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold">Extras</h4>
|
||||||
|
<ul className="pl-4 list-disc">
|
||||||
|
{type.extras.map((ext) => (
|
||||||
|
<li key={ext.id}>
|
||||||
|
{ext.extra?.name}:{" "}
|
||||||
|
{ext.price !== null
|
||||||
|
? `${ext.price}€`
|
||||||
|
: ext.pricePercent
|
||||||
|
? `+${ext.pricePercent}%`
|
||||||
|
: ext.priceRange
|
||||||
|
? `${ext.priceRange}€`
|
||||||
|
: "Included"}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
89
src/components/items/commissions/types/NewTypeForm.tsx
Normal file
89
src/components/items/commissions/types/NewTypeForm.tsx
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { createCommissionType } from "@/actions/items/commissions/types/newType";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { CommissionExtra, CommissionOption } from "@/generated/prisma";
|
||||||
|
import { commissionTypeSchema } from "@/schemas/commissionType";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import * as z from "zod/v4";
|
||||||
|
import { CommissionExtraField } from "./form/CommissionExtraField";
|
||||||
|
import { CommissionOptionField } from "./form/CommissionOptionField";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
options: CommissionOption[],
|
||||||
|
extras: CommissionExtra[],
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NewTypeForm({ options, extras }: Props) {
|
||||||
|
const router = useRouter();
|
||||||
|
const form = useForm<z.infer<typeof commissionTypeSchema>>({
|
||||||
|
resolver: zodResolver(commissionTypeSchema),
|
||||||
|
defaultValues: {
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
options: [],
|
||||||
|
extras: [],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
async function onSubmit(values: z.infer<typeof commissionTypeSchema>) {
|
||||||
|
try {
|
||||||
|
const created = await createCommissionType(values)
|
||||||
|
console.log("CommissionType created:", created)
|
||||||
|
router.push("/items/commissions/types")
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
toast("Failed to create commission type.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-8">
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>The name of the commission type.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="description"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Description</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>Optional description.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CommissionOptionField options={options} />
|
||||||
|
<CommissionExtraField extras={extras} />
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<Button type="submit">Submit</Button>
|
||||||
|
<Button type="reset" variant="secondary" onClick={() => router.back()}>Cancel</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,111 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
} from "@/components/ui/command"
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Check, ChevronsUpDown, PlusCircle } from "lucide-react"
|
||||||
|
import { useState } from "react"
|
||||||
|
|
||||||
|
type Option = {
|
||||||
|
label: string
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
options: Option[]
|
||||||
|
selected: string | undefined
|
||||||
|
onSelect: (value: string) => void
|
||||||
|
onCreateNew: (name: string) => void | Promise<void>
|
||||||
|
placeholder?: string
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ComboboxCreateable({
|
||||||
|
options,
|
||||||
|
selected,
|
||||||
|
onSelect,
|
||||||
|
onCreateNew,
|
||||||
|
placeholder = "Select or create…",
|
||||||
|
disabled = false,
|
||||||
|
}: Props) {
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const [input, setInput] = useState("")
|
||||||
|
|
||||||
|
const selectedOption = options.find((o) => o.value === selected)
|
||||||
|
|
||||||
|
const filteredOptions = input
|
||||||
|
? options.filter((opt) =>
|
||||||
|
opt.label.toLowerCase().includes(input.toLowerCase())
|
||||||
|
)
|
||||||
|
: options
|
||||||
|
|
||||||
|
const showCreate = input && !options.some((o) => o.label.toLowerCase() === input.toLowerCase())
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
className="w-full justify-between"
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{selectedOption?.label || placeholder}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-[300px] p-0">
|
||||||
|
<Command>
|
||||||
|
<CommandInput
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={input}
|
||||||
|
onValueChange={setInput}
|
||||||
|
/>
|
||||||
|
<CommandEmpty>No results</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{filteredOptions.map((opt) => (
|
||||||
|
<CommandItem
|
||||||
|
key={opt.value}
|
||||||
|
onSelect={() => {
|
||||||
|
onSelect(opt.value)
|
||||||
|
setOpen(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-4 w-4",
|
||||||
|
selected === opt.value ? "opacity-100" : "opacity-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{opt.label}
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
{showCreate && (
|
||||||
|
<CommandItem
|
||||||
|
onSelect={async () => {
|
||||||
|
await onCreateNew(input)
|
||||||
|
setOpen(false)
|
||||||
|
}}
|
||||||
|
className="text-primary"
|
||||||
|
>
|
||||||
|
<PlusCircle className="mr-2 h-4 w-4" />
|
||||||
|
Create “{input}”
|
||||||
|
</CommandItem>
|
||||||
|
)}
|
||||||
|
</CommandGroup>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,159 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { createCommissionExtra } from "@/actions/items/commissions/types/newType"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { DualRangeSlider } from "@/components/ui/dual-range"
|
||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
} from "@/components/ui/form"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { CommissionExtra } from "@/generated/prisma"
|
||||||
|
import { useState } from "react"
|
||||||
|
import { useFieldArray, useFormContext } from "react-hook-form"
|
||||||
|
import { ComboboxCreateable } from "./ComboboxCreateable"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
extras: CommissionExtra[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CommissionExtraField({ extras: initialExtras }: Props) {
|
||||||
|
const { control } = useFormContext()
|
||||||
|
const { fields, append, remove } = useFieldArray({
|
||||||
|
control,
|
||||||
|
name: "extras",
|
||||||
|
})
|
||||||
|
|
||||||
|
const [extras, setExtras] = useState(initialExtras)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-semibold">Extras</h3>
|
||||||
|
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<div key={field.id} className="grid grid-cols-5 gap-4 items-end">
|
||||||
|
{/* Extra Picker (combobox with create) */}
|
||||||
|
<FormField
|
||||||
|
control={control}
|
||||||
|
name={`extras.${index}.extraId`}
|
||||||
|
render={({ field: extraField }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Extra</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<ComboboxCreateable
|
||||||
|
options={extras.map((e) => ({
|
||||||
|
label: e.name,
|
||||||
|
value: e.id,
|
||||||
|
}))}
|
||||||
|
selected={extraField.value}
|
||||||
|
onSelect={extraField.onChange}
|
||||||
|
onCreateNew={async (name) => {
|
||||||
|
const newExtra = await createCommissionExtra({ name })
|
||||||
|
setExtras((prev) => [...prev, newExtra])
|
||||||
|
extraField.onChange(newExtra.id)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Price */}
|
||||||
|
<FormField
|
||||||
|
control={control}
|
||||||
|
name={`extras.${index}.price`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Price (€)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
field.onChange(e.target.value === "" ? undefined : e.target.valueAsNumber)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Price Percent */}
|
||||||
|
<FormField
|
||||||
|
control={control}
|
||||||
|
name={`extras.${index}.pricePercent`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>+ %</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
field.onChange(e.target.value === "" ? undefined : e.target.valueAsNumber)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Range Slider */}
|
||||||
|
<FormField
|
||||||
|
control={control}
|
||||||
|
name={`extras.${index}.priceRange`}
|
||||||
|
render={({ field }) => {
|
||||||
|
const [start, end] =
|
||||||
|
typeof field.value === "string" && field.value.includes("–")
|
||||||
|
? field.value.split("–").map(Number)
|
||||||
|
: [0, 0]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Range</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<DualRangeSlider
|
||||||
|
value={[start, end]}
|
||||||
|
onChange={([min, max]) =>
|
||||||
|
field.onChange(`${Math.min(min, max)}–${Math.max(min, max)}`)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Remove Button */}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => remove(index)}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
append({
|
||||||
|
extraId: "",
|
||||||
|
price: undefined,
|
||||||
|
pricePercent: undefined,
|
||||||
|
priceRange: "0–0",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Add Extra
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,160 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { createCommissionOption } from "@/actions/items/commissions/types/newType"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { DualRangeSlider } from "@/components/ui/dual-range"
|
||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
} from "@/components/ui/form"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { CommissionOption } from "@/generated/prisma"
|
||||||
|
import { useState } from "react"
|
||||||
|
import { useFieldArray, useFormContext } from "react-hook-form"
|
||||||
|
import { ComboboxCreateable } from "./ComboboxCreateable"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
options: CommissionOption[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CommissionOptionField({ options: initialOptions }: Props) {
|
||||||
|
const { control } = useFormContext()
|
||||||
|
const { fields, append, remove } = useFieldArray({
|
||||||
|
control,
|
||||||
|
name: "options",
|
||||||
|
})
|
||||||
|
|
||||||
|
const [options, setOptions] = useState(initialOptions)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-semibold">Options</h3>
|
||||||
|
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<div key={field.id} className="grid grid-cols-5 gap-4 items-end">
|
||||||
|
{/* Option Picker (combobox with create) */}
|
||||||
|
<FormField
|
||||||
|
control={control}
|
||||||
|
name={`options.${index}.optionId`}
|
||||||
|
render={({ field: optionField }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Option</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<ComboboxCreateable
|
||||||
|
options={options.map((o) => ({
|
||||||
|
label: o.name,
|
||||||
|
value: o.id,
|
||||||
|
}))}
|
||||||
|
selected={optionField.value}
|
||||||
|
onSelect={optionField.onChange}
|
||||||
|
onCreateNew={async (name) => {
|
||||||
|
const newOption = await createCommissionOption({ name })
|
||||||
|
setOptions((prev) => [...prev, newOption])
|
||||||
|
optionField.onChange(newOption.id)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Price */}
|
||||||
|
<FormField
|
||||||
|
control={control}
|
||||||
|
name={`options.${index}.price`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Price (€)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
field.onChange(e.target.value === "" ? undefined : e.target.valueAsNumber)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Price Percent */}
|
||||||
|
<FormField
|
||||||
|
control={control}
|
||||||
|
name={`options.${index}.pricePercent`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>+ %</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
field.onChange(e.target.value === "" ? undefined : e.target.valueAsNumber)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Price Range Slider */}
|
||||||
|
<FormField
|
||||||
|
control={control}
|
||||||
|
name={`options.${index}.priceRange`}
|
||||||
|
render={({ field }) => {
|
||||||
|
const [start, end] =
|
||||||
|
typeof field.value === "string" && field.value.includes("–")
|
||||||
|
? field.value.split("–").map(Number)
|
||||||
|
: [0, 0]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Range</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<DualRangeSlider
|
||||||
|
value={[start, end]}
|
||||||
|
onChange={([min, max]) =>
|
||||||
|
field.onChange(`${Math.min(min, max)}–${Math.max(min, max)}`)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Remove Button */}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => remove(index)}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Add Button */}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
append({
|
||||||
|
optionId: "",
|
||||||
|
price: undefined,
|
||||||
|
pricePercent: undefined,
|
||||||
|
priceRange: "0–0",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Add Option
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
184
src/components/ui/command.tsx
Normal file
184
src/components/ui/command.tsx
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { Command as CommandPrimitive } from "cmdk"
|
||||||
|
import { SearchIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog"
|
||||||
|
|
||||||
|
function Command({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive
|
||||||
|
data-slot="command"
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandDialog({
|
||||||
|
title = "Command Palette",
|
||||||
|
description = "Search for a command to run...",
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
showCloseButton = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof Dialog> & {
|
||||||
|
title?: string
|
||||||
|
description?: string
|
||||||
|
className?: string
|
||||||
|
showCloseButton?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Dialog {...props}>
|
||||||
|
<DialogHeader className="sr-only">
|
||||||
|
<DialogTitle>{title}</DialogTitle>
|
||||||
|
<DialogDescription>{description}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogContent
|
||||||
|
className={cn("overflow-hidden p-0", className)}
|
||||||
|
showCloseButton={showCloseButton}
|
||||||
|
>
|
||||||
|
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||||
|
{children}
|
||||||
|
</Command>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandInput({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="command-input-wrapper"
|
||||||
|
className="flex h-9 items-center gap-2 border-b px-3"
|
||||||
|
>
|
||||||
|
<SearchIcon className="size-4 shrink-0 opacity-50" />
|
||||||
|
<CommandPrimitive.Input
|
||||||
|
data-slot="command-input"
|
||||||
|
className={cn(
|
||||||
|
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandList({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.List
|
||||||
|
data-slot="command-list"
|
||||||
|
className={cn(
|
||||||
|
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandEmpty({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Empty
|
||||||
|
data-slot="command-empty"
|
||||||
|
className="py-6 text-center text-sm"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandGroup({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Group
|
||||||
|
data-slot="command-group"
|
||||||
|
className={cn(
|
||||||
|
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Separator
|
||||||
|
data-slot="command-separator"
|
||||||
|
className={cn("bg-border -mx-1 h-px", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandItem({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Item
|
||||||
|
data-slot="command-item"
|
||||||
|
className={cn(
|
||||||
|
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandShortcut({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="command-shortcut"
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Command,
|
||||||
|
CommandDialog,
|
||||||
|
CommandInput,
|
||||||
|
CommandList,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandItem,
|
||||||
|
CommandShortcut,
|
||||||
|
CommandSeparator,
|
||||||
|
}
|
39
src/components/ui/dual-range.tsx
Normal file
39
src/components/ui/dual-range.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as SliderPrimitive from "@radix-ui/react-slider";
|
||||||
|
|
||||||
|
type DualRangeProps = {
|
||||||
|
value: [number, number];
|
||||||
|
onChange: (val: [number, number]) => void;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
step?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function DualRangeSlider({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
min = 0,
|
||||||
|
max = 200,
|
||||||
|
step = 1,
|
||||||
|
}: DualRangeProps) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="text-sm mb-1">Range: {value[0]}–{value[1]}</div>
|
||||||
|
<SliderPrimitive.Root
|
||||||
|
className="relative flex w-full touch-none select-none items-center"
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
step={step}
|
||||||
|
value={value}
|
||||||
|
onValueChange={(val) => onChange([val[0], val[1]])}
|
||||||
|
>
|
||||||
|
<SliderPrimitive.Track className="relative h-1 w-full grow overflow-hidden rounded-full bg-muted">
|
||||||
|
<SliderPrimitive.Range className="absolute h-full bg-primary" />
|
||||||
|
</SliderPrimitive.Track>
|
||||||
|
<SliderPrimitive.Thumb className="block h-4 w-4 rounded-full bg-white border border-primary shadow transition-colors focus:outline-none" />
|
||||||
|
<SliderPrimitive.Thumb className="block h-4 w-4 rounded-full bg-white border border-primary shadow transition-colors focus:outline-none" />
|
||||||
|
</SliderPrimitive.Root>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
48
src/components/ui/popover.tsx
Normal file
48
src/components/ui/popover.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Popover({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
||||||
|
return <PopoverPrimitive.Root data-slot="popover" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
||||||
|
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverContent({
|
||||||
|
className,
|
||||||
|
align = "center",
|
||||||
|
sideOffset = 4,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<PopoverPrimitive.Portal>
|
||||||
|
<PopoverPrimitive.Content
|
||||||
|
data-slot="popover-content"
|
||||||
|
align={align}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</PopoverPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverAnchor({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
||||||
|
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
25
src/components/ui/sonner.tsx
Normal file
25
src/components/ui/sonner.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useTheme } from "next-themes"
|
||||||
|
import { Toaster as Sonner, ToasterProps } from "sonner"
|
||||||
|
|
||||||
|
const Toaster = ({ ...props }: ToasterProps) => {
|
||||||
|
const { theme = "system" } = useTheme()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sonner
|
||||||
|
theme={theme as ToasterProps["theme"]}
|
||||||
|
className="toaster group"
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--normal-bg": "var(--popover)",
|
||||||
|
"--normal-text": "var(--popover-foreground)",
|
||||||
|
"--normal-border": "var(--border)",
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Toaster }
|
14
src/lib/prisma.ts
Normal file
14
src/lib/prisma.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
// import { PrismaClient } from '@/types/prisma'
|
||||||
|
// import { withAccelerate } from '@prisma/extension-accelerate'
|
||||||
|
|
||||||
|
import { PrismaClient } from "@/generated/prisma"
|
||||||
|
|
||||||
|
const globalForPrisma = global as unknown as {
|
||||||
|
prisma: PrismaClient
|
||||||
|
}
|
||||||
|
|
||||||
|
const prisma = globalForPrisma.prisma || new PrismaClient()
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
|
||||||
|
|
||||||
|
export default prisma
|
26
src/schemas/commissionType.ts
Normal file
26
src/schemas/commissionType.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import * as z from "zod/v4";
|
||||||
|
|
||||||
|
const rangePattern = /^\d{1,3}–\d{1,3}$/;
|
||||||
|
|
||||||
|
const optionField = z.object({
|
||||||
|
optionId: z.string(),
|
||||||
|
price: z.number().optional(),
|
||||||
|
pricePercent: z.number().optional(),
|
||||||
|
priceRange: z.string().regex(rangePattern, "Format must be like '10–80'").optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const extraField = z.object({
|
||||||
|
extraId: z.string(),
|
||||||
|
price: z.number().optional(),
|
||||||
|
pricePercent: z.number().optional(),
|
||||||
|
priceRange: z.string().regex(rangePattern, "Format must be like '10–80'").optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const commissionTypeSchema = z.object({
|
||||||
|
name: z.string().min(1, "Name is required. Min 1 character."),
|
||||||
|
description: z.string().optional(),
|
||||||
|
options: z.array(optionField).optional(),
|
||||||
|
extras: z.array(extraField).optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type commissionTypeSchema = z.infer<typeof commissionTypeSchema>
|
Reference in New Issue
Block a user