From bbaf4aa35a828bebdf35d469ae2b4ae5e4b05454 Mon Sep 17 00:00:00 2001 From: NotCayaa Date: Sun, 22 Feb 2026 21:19:17 +0700 Subject: [PATCH 01/11] nyambungin db + engine ke web app, buat testing output. sekalian gw benerin tombol cancel + routing hooks tengs --- .../(auth)/login/components/login-form.tsx | 53 +++- .../register/components/signup-form.tsx | 60 +++- .../project/components/NewProjectLayout.tsx | 8 +- .../project/components/StepContent.tsx | 14 +- .../project/components/steps/ReviewStep.tsx | 263 +++++++++++++++++- .../steps/webApp/DetailsScopeStep.tsx | 37 ++- .../steps/webApp/ProjectInfoStep.tsx | 4 +- apps/web-app/features/project/types.ts | 6 + .../steps/webApp => }/hooks/index.ts | 0 .../webApp => }/hooks/useGenerateProject.ts | 4 +- .../steps/webApp => }/hooks/useWebAppForm.ts | 2 +- 11 files changed, 417 insertions(+), 34 deletions(-) rename apps/web-app/{features/project/components/steps/webApp => }/hooks/index.ts (100%) rename apps/web-app/{features/project/components/steps/webApp => }/hooks/useGenerateProject.ts (95%) rename apps/web-app/{features/project/components/steps/webApp => }/hooks/useWebAppForm.ts (96%) diff --git a/apps/web-app/app/(auth)/login/components/login-form.tsx b/apps/web-app/app/(auth)/login/components/login-form.tsx index 4940752..9114477 100644 --- a/apps/web-app/app/(auth)/login/components/login-form.tsx +++ b/apps/web-app/app/(auth)/login/components/login-form.tsx @@ -12,20 +12,52 @@ import { import { Input } from "@workspace/ui/components/input"; import Link from "next/link"; import { useRouter } from "next/navigation"; +import { useState } from "react"; + +const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:4000/api" export function LoginForm({ className, ...props }: React.ComponentProps<"form">) { - const router = useRouter() + const router = useRouter() + const [error, setError] = useState(null) + const [isLoading, setIsLoading] = useState(false) + + const onSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setError(null) + setIsLoading(true) + + const form = e.target as HTMLFormElement + const email = (form.elements.namedItem("email") as HTMLInputElement).value + const password = (form.elements.namedItem("password") as HTMLInputElement).value + + try { + const res = await fetch(`${API_BASE_URL}/auth/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password }), + }) + + const data = await res.json() - const onSubmit = async (e: React.FormEvent) => { - e.preventDefault() + if (!res.ok) { + setError(data.message || "Login failed") + return + } - // logic be + // Store token and user data + localStorage.setItem("token", data.data.token) + localStorage.setItem("user", JSON.stringify(data.data.user)) - router.push("/dashboard") + router.push("/dashboard") + } catch (err: any) { + setError(err.message || "Network error. Is the backend running?") + } finally { + setIsLoading(false) } + } return (
@@ -51,8 +83,17 @@ export function LoginForm({ + + {error && ( +
+

{error}

+
+ )} + - + Or continue with diff --git a/apps/web-app/app/(auth)/register/components/signup-form.tsx b/apps/web-app/app/(auth)/register/components/signup-form.tsx index 35d542e..30b1865 100644 --- a/apps/web-app/app/(auth)/register/components/signup-form.tsx +++ b/apps/web-app/app/(auth)/register/components/signup-form.tsx @@ -12,22 +12,59 @@ import { import { Input } from "@workspace/ui/components/input" import Link from "next/link" import { useRouter } from "next/navigation" +import { useState } from "react" + +const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:4000/api" export function SignupForm({ className, ...props }: React.ComponentProps<"form">) { - const router = useRouter() + const router = useRouter() + const [error, setError] = useState(null) + const [isLoading, setIsLoading] = useState(false) - const onSubmit = async (e: React.FormEvent) => { - e.preventDefault() + const onSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setError(null) - // await fetch apahasdoa + const form = e.target as HTMLFormElement + const name = (form.elements.namedItem("name") as HTMLInputElement).value + const email = (form.elements.namedItem("email") as HTMLInputElement).value + const password = (form.elements.namedItem("password") as HTMLInputElement).value + const confirmPassword = (form.elements.namedItem("confirm-password") as HTMLInputElement).value - router.push("/otp") + if (password !== confirmPassword) { + setError("Passwords do not match") + return } - + + setIsLoading(true) + + try { + const res = await fetch(`${API_BASE_URL}/auth/register`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password, name }), + }) + + const data = await res.json() + + if (!res.ok) { + setError(data.message || "Registration failed") + return + } + + // Registration successful — redirect to login + router.push("/login") + } catch (err: any) { + setError(err.message || "Network error. Is the backend running?") + } finally { + setIsLoading(false) + } + } + return ( @@ -61,8 +98,17 @@ export function SignupForm({ Please confirm your password. + + {error && ( +
+

{error}

+
+ )} + - + Or continue with diff --git a/apps/web-app/features/project/components/NewProjectLayout.tsx b/apps/web-app/features/project/components/NewProjectLayout.tsx index 7e0f0ee..c47021a 100644 --- a/apps/web-app/features/project/components/NewProjectLayout.tsx +++ b/apps/web-app/features/project/components/NewProjectLayout.tsx @@ -6,13 +6,15 @@ import type { WebAppFormData } from "../types" import { StepSidebar } from "./StepSidebar" import { StepContent } from "./StepContent" import { ReviewSidebar } from "./reviewSidebar" -import { useWebAppForm } from "./steps/webApp/hooks" +import { useWebAppForm } from "../../../hooks" +import { useGenerateProject } from "../../../hooks" export function NewProjectLayout() { const [type, setType] = useState(null) const [step, setStep] = useState(1) const { formData, updateField, resetForm, canProceedToDetails, canProceedToReview } = useWebAppForm() + const { isLoading: isGenerating, error: generateError, result: generateResult, generate, clearError } = useGenerateProject() const isReview = step === 4 @@ -33,6 +35,10 @@ export function NewProjectLayout() { updateField={updateField} canProceedToDetails={canProceedToDetails} canProceedToReview={canProceedToReview} + onGenerate={generate} + isGenerating={isGenerating} + generateError={generateError} + generateResult={generateResult} /> diff --git a/apps/web-app/features/project/components/StepContent.tsx b/apps/web-app/features/project/components/StepContent.tsx index 70fdb29..3486e6c 100644 --- a/apps/web-app/features/project/components/StepContent.tsx +++ b/apps/web-app/features/project/components/StepContent.tsx @@ -1,6 +1,6 @@ import { stepRegistry } from "../step.registry" import { ProjectType } from "../types" -import type { WebAppFormData } from "../types" +import type { WebAppFormData, GenerateProjectResponse } from "../types" interface Props { type: ProjectType | null @@ -11,6 +11,10 @@ interface Props { updateField: (field: K, value: WebAppFormData[K]) => void canProceedToDetails: boolean canProceedToReview: boolean + onGenerate: (formData: WebAppFormData, includeDiagram?: boolean) => Promise + isGenerating: boolean + generateError: string | null + generateResult: GenerateProjectResponse | null } export function StepContent({ @@ -22,6 +26,10 @@ export function StepContent({ updateField, canProceedToDetails, canProceedToReview, + onGenerate, + isGenerating, + generateError, + generateResult, }: Props) { const StepComponent = type ? stepRegistry[type]?.[step] : stepRegistry.web_app[1] @@ -37,6 +45,10 @@ export function StepContent({ updateField={updateField} canProceedToDetails={canProceedToDetails} canProceedToReview={canProceedToReview} + onGenerate={onGenerate} + isGenerating={isGenerating} + generateError={generateError} + generateResult={generateResult} /> ) diff --git a/apps/web-app/features/project/components/steps/ReviewStep.tsx b/apps/web-app/features/project/components/steps/ReviewStep.tsx index c2374cc..d2ead81 100644 --- a/apps/web-app/features/project/components/steps/ReviewStep.tsx +++ b/apps/web-app/features/project/components/steps/ReviewStep.tsx @@ -1,15 +1,262 @@ "use client"; -export function ReviewStep() { +import type { GenerateProjectResponse } from "../../types"; +import { Loader2 } from "lucide-react"; + +interface Props { + isGenerating?: boolean; + generateError?: string | null; + generateResult?: GenerateProjectResponse | null; +} + +export function ReviewStep({ isGenerating, generateError, generateResult }: Props) { + + // Loading state + if (isGenerating) { + return ( +
+
+ +

Generating your project brief...

+

This may take a moment depending on document style

+
+
+ ); + } + + // Error state + if (generateError) { + return ( +
+
+
+ +
+

Generation Failed

+

{generateError}

+
+
+ ); + } + + // No result yet (shouldn't happen normally, but safety) + if (!generateResult) { + return ( +
+
+ {" "} + {/* Kosong dulu */} +
+
+ {" "} + {/* Kosong dulu */} +
+
+ ); + } + + // Render AI result + const brief = generateResult; + return ( -
-
- {" "} - {/* Kosong dulu */} +
+ + {/* Page 1 — Title, Overview, Problem, Objectives */} +
+ + {/* Watermark */} +

PREVIEW — Planify AI

+ + {/* Title */} + {brief.title && ( +
+

{brief.title}

+
+ )} + + {/* Overview */} + {brief.overview && ( +
+

Overview

+

{brief.overview}

+
+ )} + + {/* Target Audience */} + {brief.targetAudience && ( +
+

Target Audience

+ {typeof brief.targetAudience === "string" ? ( +

{brief.targetAudience}

+ ) : ( +
+ {brief.targetAudience.primary &&

Primary: {brief.targetAudience.primary}

} + {brief.targetAudience.secondary &&

Secondary: {brief.targetAudience.secondary}

} + {brief.targetAudience.admin &&

Admin: {brief.targetAudience.admin}

} +
+ )} +
+ )} + + {/* Problem Statement */} + {brief.problemStatement && ( +
+

Problem Statement

+ {brief.problemStatement.painPoints && ( +
    + {brief.problemStatement.painPoints.map((p: string, i: number) => ( +
  • {p}
  • + ))} +
+ )} + {brief.problemStatement.businessImpact && ( +
+

Business Impact

+

{brief.problemStatement.businessImpact}

+
+ )} +
+ )} + + {/* Objectives */} + {brief.objectives && Array.isArray(brief.objectives) && ( +
+

Objectives

+
    + {brief.objectives.map((obj: any, i: number) => ( +
  • {typeof obj === "string" ? obj : obj.objective || JSON.stringify(obj)}
  • + ))} +
+
+ )} + + {/* Success Criteria */} + {brief.successCriteria && Array.isArray(brief.successCriteria) && ( +
+

Success Criteria

+
+ {brief.successCriteria.map((sc: any, i: number) => ( +
+

{sc.metric}

+

{sc.target}

+
+ ))} +
+
+ )}
-
- {" "} - {/* Kosong dulu */} + + {/* Page 2 — Features, Tech Stack, Scope, Risks */} +
+ +

PREVIEW — Planify AI

+ + {/* Key Features */} + {brief.keyFeatures && Array.isArray(brief.keyFeatures) && ( +
+

Key Features

+
+ {brief.keyFeatures.map((f: any, i: number) => ( +
+
+

{f.name}

+ {f.priority && ( + + {f.priority} + + )} +
+

{f.description}

+ {f.userStory &&

{f.userStory}

} +
+ ))} +
+
+ )} + + {/* Tech Stack */} + {brief.recommendedTechStack && Array.isArray(brief.recommendedTechStack) && ( +
+

Recommended Tech Stack

+
+ {brief.recommendedTechStack.map((t: any, i: number) => ( +
+ {t.category} +
+

{t.technology}

+

{t.reason}

+
+
+ ))} +
+
+ )} + + {/* Scope */} + {brief.scope && ( +
+

Scope

+
+ {brief.scope.inScope && ( +
+

In Scope

+
    + {brief.scope.inScope.map((s: string, i: number) => ( +
  • {s}
  • + ))} +
+
+ )} + {brief.scope.outOfScope && ( +
+

Out of Scope

+
    + {brief.scope.outOfScope.map((s: string, i: number) => ( +
  • {s}
  • + ))} +
+
+ )} +
+
+ )} + + {/* Risks */} + {brief.risks && Array.isArray(brief.risks) && ( +
+

Risks

+
+ {brief.risks.map((r: any, i: number) => ( +
+
+ {r.type} +

{r.risk}

+
+

Mitigation: {r.mitigation}

+
+ ))} +
+
+ )} + + {/* Clarification Log */} + {brief.clarificationLog && Array.isArray(brief.clarificationLog) && ( +
+

AI Clarification Log

+
+ {brief.clarificationLog.map((c: any, i: number) => ( +
+
+ {c.date} +

{c.topic}

+
+

{c.advice}

+
+ ))} +
+
+ )} +
); diff --git a/apps/web-app/features/project/components/steps/webApp/DetailsScopeStep.tsx b/apps/web-app/features/project/components/steps/webApp/DetailsScopeStep.tsx index 816769b..f6860b5 100644 --- a/apps/web-app/features/project/components/steps/webApp/DetailsScopeStep.tsx +++ b/apps/web-app/features/project/components/steps/webApp/DetailsScopeStep.tsx @@ -5,7 +5,7 @@ import { Field, FieldLabel } from "@workspace/ui/components/field"; import { Input } from "@workspace/ui/components/input"; import { Label } from "@workspace/ui/components/label"; import { Separator } from "@workspace/ui/components/separator"; -import { ArrowRight, Cpu, Goal, ListTodo } from "lucide-react"; +import { ArrowRight, Cpu, Goal, ListTodo, Loader2 } from "lucide-react"; import { TagInput } from "./components/tagInput"; import { Button } from "@workspace/ui/components/button"; import type { WebAppFormData } from "../../../types"; @@ -15,9 +15,18 @@ interface Props { formData: WebAppFormData; updateField: (field: K, value: WebAppFormData[K]) => void; canProceedToReview: boolean; + onGenerate: (formData: WebAppFormData, includeDiagram?: boolean) => Promise; + isGenerating: boolean; + generateError: string | null; } -export function DetailsScopeStep({ setStep, formData, updateField, canProceedToReview }: Props) { +export function DetailsScopeStep({ setStep, formData, updateField, canProceedToReview, onGenerate, isGenerating, generateError }: Props) { + + const handleContinueToReview = async () => { + await onGenerate(formData) + setStep(4) + } + return (
@@ -142,17 +151,33 @@ export function DetailsScopeStep({ setStep, formData, updateField, canProceedToR
+ + {generateError && ( +
+

{generateError}

+
+ )}
); } diff --git a/apps/web-app/features/project/components/steps/webApp/ProjectInfoStep.tsx b/apps/web-app/features/project/components/steps/webApp/ProjectInfoStep.tsx index f23aaaa..b655f81 100644 --- a/apps/web-app/features/project/components/steps/webApp/ProjectInfoStep.tsx +++ b/apps/web-app/features/project/components/steps/webApp/ProjectInfoStep.tsx @@ -34,7 +34,7 @@ export function ProjectInfoStep({ setStep, formData, updateField, canProceedToDe description: string; }[] = [ { - value: "profesional", + value: "professional", title: "Professional", description: "Standar business tone, balanced detail.", }, @@ -128,7 +128,7 @@ export function ProjectInfoStep({ setStep, formData, updateField, canProceedToDe
diff --git a/apps/web-app/features/project/types.ts b/apps/web-app/features/project/types.ts index 9a6bfd2..ca9c064 100644 --- a/apps/web-app/features/project/types.ts +++ b/apps/web-app/features/project/types.ts @@ -43,15 +43,21 @@ export interface GenerateProjectInput { } export interface GenerateProjectResponse { + title?: string overview?: any + targetAudience?: any problemStatement?: any objectives?: any + successCriteria?: any keyFeatures?: any userFlow?: any srsModules?: any recommendedTechStack?: any + nonFunctionalRequirements?: any scope?: any risks?: any + assumptions?: any clarificationLog?: any diagram?: any + [key: string]: any // Allow any additional fields from AI } diff --git a/apps/web-app/features/project/components/steps/webApp/hooks/index.ts b/apps/web-app/hooks/index.ts similarity index 100% rename from apps/web-app/features/project/components/steps/webApp/hooks/index.ts rename to apps/web-app/hooks/index.ts diff --git a/apps/web-app/features/project/components/steps/webApp/hooks/useGenerateProject.ts b/apps/web-app/hooks/useGenerateProject.ts similarity index 95% rename from apps/web-app/features/project/components/steps/webApp/hooks/useGenerateProject.ts rename to apps/web-app/hooks/useGenerateProject.ts index 8735baa..3190056 100644 --- a/apps/web-app/features/project/components/steps/webApp/hooks/useGenerateProject.ts +++ b/apps/web-app/hooks/useGenerateProject.ts @@ -1,8 +1,8 @@ "use client" import { useState, useCallback } from "react" -import { generateProjectBrief } from "../../../../services/project.service" -import type { WebAppFormData, GenerateProjectInput, GenerateProjectResponse } from "../../../../types" +import { generateProjectBrief } from "../features/project/services/project.service" +import type { WebAppFormData, GenerateProjectInput, GenerateProjectResponse } from "../features/project/types" interface UseGenerateProjectReturn { isLoading: boolean diff --git a/apps/web-app/features/project/components/steps/webApp/hooks/useWebAppForm.ts b/apps/web-app/hooks/useWebAppForm.ts similarity index 96% rename from apps/web-app/features/project/components/steps/webApp/hooks/useWebAppForm.ts rename to apps/web-app/hooks/useWebAppForm.ts index 591366b..a13c621 100644 --- a/apps/web-app/features/project/components/steps/webApp/hooks/useWebAppForm.ts +++ b/apps/web-app/hooks/useWebAppForm.ts @@ -1,7 +1,7 @@ "use client" import { useState, useCallback } from "react" -import type { WebAppFormData, DocumentStyle, OutputLanguage, ProjectStatus } from "../../../../types" +import type { WebAppFormData, DocumentStyle, OutputLanguage, ProjectStatus } from "../features/project/types" const initialFormData: WebAppFormData = { // Step 1 From b543b3e8fa957a4624cc5de429efb85127b7c217 Mon Sep 17 00:00:00 2001 From: NotCayaa Date: Sun, 22 Feb 2026 21:58:49 +0700 Subject: [PATCH 02/11] nambah engine export markdown --- apps/web-api/public/index.html | 59 ++- apps/web-api/src/app.ts | 2 + .../src/controllers/markdown.controller.ts | 64 +++ apps/web-api/src/routes/markdown.route.ts | 14 + .../web-api/src/services/coin/coin.service.ts | 1 + .../src/services/markdown/markdown.service.ts | 383 ++++++++++++++++++ 6 files changed, 516 insertions(+), 7 deletions(-) create mode 100644 apps/web-api/src/controllers/markdown.controller.ts create mode 100644 apps/web-api/src/routes/markdown.route.ts create mode 100644 apps/web-api/src/services/markdown/markdown.service.ts diff --git a/apps/web-api/public/index.html b/apps/web-api/public/index.html index 11fc7bc..21b60e9 100644 --- a/apps/web-api/public/index.html +++ b/apps/web-api/public/index.html @@ -327,7 +327,6 @@ -