diff --git a/packages/backend/prisma/schema.prisma b/packages/backend/prisma/schema.prisma index 27e7835..7517d75 100644 --- a/packages/backend/prisma/schema.prisma +++ b/packages/backend/prisma/schema.prisma @@ -258,3 +258,11 @@ enum ProofStatus { enum NotificationType { COMMITMENT_RECEIVED } + +model FeatureRequest { + id String @id @default(cuid()) + content String + createdAt DateTime @default(now()) @map("created_at") + + @@map("feature_requests") +} diff --git a/packages/backend/src/app.module.ts b/packages/backend/src/app.module.ts index 707b2cc..19e249f 100644 --- a/packages/backend/src/app.module.ts +++ b/packages/backend/src/app.module.ts @@ -12,6 +12,7 @@ import { EventsModule } from './events/events.module'; import { NotificationModule } from './notification/notification.module'; import { AuthModule } from './auth/auth.module'; import { PriceModule } from './price/price.module'; +import { FeatureRequestModule } from './feature-request/feature-request.module'; @Module({ imports: [ @@ -28,6 +29,7 @@ import { PriceModule } from './price/price.module'; NotificationModule, AuthModule, PriceModule, + FeatureRequestModule, ], }) export class AppModule {} diff --git a/packages/backend/src/feature-request/feature-request.controller.ts b/packages/backend/src/feature-request/feature-request.controller.ts new file mode 100644 index 0000000..a296caa --- /dev/null +++ b/packages/backend/src/feature-request/feature-request.controller.ts @@ -0,0 +1,29 @@ +import { Controller, Get, Post, Body } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse, ApiBody } from '@nestjs/swagger'; +import { FeatureRequestService } from './feature-request.service'; +import { CreateFeatureRequestDto } from '@polypay/shared'; + +@ApiTags('feature-requests') +@Controller('feature-requests') +export class FeatureRequestController { + constructor(private readonly featureRequestService: FeatureRequestService) {} + + @Post() + @ApiOperation({ summary: 'Submit a new feature request' }) + @ApiBody({ type: CreateFeatureRequestDto }) + @ApiResponse({ + status: 201, + description: 'Feature request submitted successfully', + }) + @ApiResponse({ status: 400, description: 'Bad request' }) + async create(@Body() dto: CreateFeatureRequestDto) { + return this.featureRequestService.create(dto); + } + + @Get() + @ApiOperation({ summary: 'Get all feature requests (internal)' }) + @ApiResponse({ status: 200, description: 'List of feature requests' }) + async findAll() { + return this.featureRequestService.findAll(); + } +} diff --git a/packages/backend/src/feature-request/feature-request.module.ts b/packages/backend/src/feature-request/feature-request.module.ts new file mode 100644 index 0000000..2c6c70d --- /dev/null +++ b/packages/backend/src/feature-request/feature-request.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { FeatureRequestController } from './feature-request.controller'; +import { FeatureRequestService } from './feature-request.service'; +import { DatabaseModule } from '@/database/database.module'; + +@Module({ + imports: [DatabaseModule], + controllers: [FeatureRequestController], + providers: [FeatureRequestService], + exports: [FeatureRequestService], +}) +export class FeatureRequestModule {} diff --git a/packages/backend/src/feature-request/feature-request.service.ts b/packages/backend/src/feature-request/feature-request.service.ts new file mode 100644 index 0000000..f43a975 --- /dev/null +++ b/packages/backend/src/feature-request/feature-request.service.ts @@ -0,0 +1,27 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { PrismaService } from '@/database/prisma.service'; +import { CreateFeatureRequestDto } from '@polypay/shared'; + +@Injectable() +export class FeatureRequestService { + private readonly logger = new Logger(FeatureRequestService.name); + + constructor(private prisma: PrismaService) {} + + async create(dto: CreateFeatureRequestDto) { + const featureRequest = await this.prisma.featureRequest.create({ + data: { + content: dto.content, + }, + }); + + this.logger.log(`Created feature request: ${featureRequest.id}`); + return featureRequest; + } + + async findAll() { + return this.prisma.featureRequest.findMany({ + orderBy: { createdAt: 'desc' }, + }); + } +} diff --git a/packages/nextjs/components/Sidebar/Sidebar.tsx b/packages/nextjs/components/Sidebar/Sidebar.tsx index 28b6725..07f28db 100644 --- a/packages/nextjs/components/Sidebar/Sidebar.tsx +++ b/packages/nextjs/components/Sidebar/Sidebar.tsx @@ -187,12 +187,9 @@ export default function Sidebar() { {/* Bottom Section */}
- {/* Request new feature */}
{ - // TODO: Open request feature modal - }} + onClick={() => openModal("requestFeature")} > Request feature Request new feature diff --git a/packages/nextjs/components/modals/ModalLayout.tsx b/packages/nextjs/components/modals/ModalLayout.tsx index e6ab49c..c91305b 100644 --- a/packages/nextjs/components/modals/ModalLayout.tsx +++ b/packages/nextjs/components/modals/ModalLayout.tsx @@ -32,6 +32,10 @@ const modals: ModalRegistry = { removeBatch: dynamic(() => import("./RemoveBatchModal"), { ssr: false, }), + + requestFeature: dynamic(() => import("./RequestFeatureModal"), { + ssr: false, + }), }; type ModalInstance = { diff --git a/packages/nextjs/components/modals/RequestFeatureModal.tsx b/packages/nextjs/components/modals/RequestFeatureModal.tsx new file mode 100644 index 0000000..679e56f --- /dev/null +++ b/packages/nextjs/components/modals/RequestFeatureModal.tsx @@ -0,0 +1,107 @@ +import Image from "next/image"; +import ModalContainer from "./ModalContainer"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { X } from "lucide-react"; +import { useForm } from "react-hook-form"; +import { toast } from "react-hot-toast"; +import { FeatureRequestFormData, featureRequestSchema } from "~~/lib/form/schemas"; +import { featureRequestApi } from "~~/services/api"; +import { ModalProps } from "~~/types/modal"; + +const BUTTON_BASE_CLASS = "text-main-black font-medium h-9 text-sm rounded-lg disabled:opacity-50"; + +const RequestFeatureModal: React.FC = ({ isOpen, onClose }) => { + const { + register, + handleSubmit, + formState: { errors, isSubmitting }, + reset, + watch, + } = useForm({ + resolver: zodResolver(featureRequestSchema), + mode: "onChange", + defaultValues: { content: "" }, + }); + + const content = watch("content"); + const isDisabled = isSubmitting || !content?.trim(); + + const onSubmit = async (data: FeatureRequestFormData) => { + try { + await featureRequestApi.create(data); + toast.success("Feature request submitted successfully!"); + reset(); + onClose(); + } catch (error) { + console.error("Error submitting feature request:", error); + toast.error("Failed to submit feature request"); + } + }; + + const handleCancel = () => { + reset(); + onClose(); + }; + + return ( + +
+
+ background +
+

Request a new feature

+ +
+
+
+