From a78df8c06e12f2904293cc60a7a3247687e63ca0 Mon Sep 17 00:00:00 2001 From: jsdevrazuislam Date: Mon, 14 Jul 2025 07:10:46 +0600 Subject: [PATCH 1/2] feature: added e2e lint setup --- .github/workflows/lint.yml | 21 + .husky/pre-commit | 1 + .vscode/settings.json | 46 + README.md | 16 +- app/api/build/route.ts | 14 +- app/builder/builder.tsx | 351 +++-- app/builder/page.tsx | 75 +- app/globals.css | 4 +- app/layout.tsx | 26 +- app/page.tsx | 76 +- components.json | 2 +- components/builder-form.tsx | 494 ++++--- components/builder-result.tsx | 342 ++--- components/error-display.tsx | 196 +-- components/featured.tsx | 59 +- components/header.tsx | 156 +- components/hero-section.tsx | 119 +- components/how-it-works.tsx | 130 +- components/testimonial.tsx | 85 +- components/ui/alert.tsx | 24 +- components/ui/badge.tsx | 24 +- components/ui/button.tsx | 26 +- components/ui/card.tsx | 34 +- components/ui/dropdown-menu.tsx | 76 +- components/ui/form.tsx | 112 +- components/ui/input.tsx | 10 +- components/ui/label.tsx | 14 +- components/ui/progress.tsx | 14 +- components/ui/select.tsx | 52 +- components/ui/textarea.tsx | 10 +- context/i18n-store.tsx | 139 +- eslint.config.mjs | 43 +- hooks/use-pc-builder.tsx | 52 + hooks/usePCBuilder.tsx | 49 - i18n/bn.ts | 108 +- lib/currency-formater.ts | 68 +- lib/gemini.ts | 53 +- lib/utils.ts | 8 +- package.json | 14 +- pnpm-lock.yaml | 2389 ++++++++++++++++++++++++++++++- tsconfig.json | 20 +- types/global.d.ts | 30 +- validation/index.ts | 10 + 43 files changed, 4026 insertions(+), 1566 deletions(-) create mode 100644 .github/workflows/lint.yml create mode 100644 .husky/pre-commit create mode 100644 .vscode/settings.json create mode 100644 hooks/use-pc-builder.tsx delete mode 100644 hooks/usePCBuilder.tsx create mode 100644 validation/index.ts diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..e20a5d2 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,21 @@ +name: Lint + +on: + pull_request: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: 22.x + - run: pnpm i + - run: pnpm lint diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..5ee7abd --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +pnpm exec lint-staged diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..c825fb7 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,46 @@ +{ + "prettier.enable": false, + "editor.formatOnSave": false, + + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit", + "source.organizeImports": "never" + }, + + "eslint.rules.customizations": [ + { "rule": "style/*", "severity": "off", "fixable": true }, + { "rule": "format/*", "severity": "off", "fixable": true }, + { "rule": "*-indent", "severity": "off", "fixable": true }, + { "rule": "*-spacing", "severity": "off", "fixable": true }, + { "rule": "*-spaces", "severity": "off", "fixable": true }, + { "rule": "*-order", "severity": "off", "fixable": true }, + { "rule": "*-dangle", "severity": "off", "fixable": true }, + { "rule": "*-newline", "severity": "off", "fixable": true }, + { "rule": "*quotes", "severity": "off", "fixable": true }, + { "rule": "*semi", "severity": "off", "fixable": true } + ], + + "eslint.validate": [ + "javascript", + "javascriptreact", + "typescript", + "typescriptreact", + "vue", + "html", + "markdown", + "json", + "jsonc", + "yaml", + "toml", + "xml", + "gql", + "graphql", + "astro", + "svelte", + "css", + "less", + "scss", + "pcss", + "postcss" + ] +} diff --git a/README.md b/README.md index c72e021..e202135 100644 --- a/README.md +++ b/README.md @@ -5,16 +5,19 @@ PCMate is an intelligent PC configuration tool that leverages AI to suggest opti ## Key Features ### AI-Powered Recommendations + - 🤖 Smart component suggestions using Google Gemini - 💡 Build optimization for different use cases (gaming, productivity, etc.) - ⚡ Real-time compatibility checking ### Multi-Language Support + - 🌐 Switch between multiple languages - 📦 Comprehensive hardware terminology translations - 🔄 Dynamic content switching ### Core Functionality + - 🛠️ Interactive PC building interface - 💰 Budget-based component filtering - 📊 Performance estimation @@ -23,6 +26,7 @@ PCMate is an intelligent PC configuration tool that leverages AI to suggest opti ## Technology Stack ### Frontend + - **Next.js 15** (App Router) - **TypeScript** - **shadcn/ui** (Beautifully designed components) @@ -32,10 +36,12 @@ PCMate is an intelligent PC configuration tool that leverages AI to suggest opti - **Zod** (Schema validation) ### Backend + - **Next.js API Routes** - **Google Gemini API** (AI recommendations) ### Development Tools + - **ESLint** + **Prettier** (Code quality) - **Husky** (Git hooks) - **GitHub Actions** (CI/CD) @@ -43,6 +49,7 @@ PCMate is an intelligent PC configuration tool that leverages AI to suggest opti ## Getting Started ### Prerequisites + - Node.js 18+ - Google Gemini API key - Git @@ -53,16 +60,22 @@ PCMate is an intelligent PC configuration tool that leverages AI to suggest opti ```bash git clone https://github.com/jsdevrazuislam/pc-mate.git cd pc-mate + ``` 2. **Install dependencies**: ```bash pnpm install + ``` 3. **Set up environment variables**: ```bash GEMINI_API_KEY=your_api_key_here + ``` 4. **Run the development server**: ```bash pnpm dev + ``` + # Project Structure + ```bash pc-mate/ ├── app/ @@ -82,4 +95,5 @@ pc-mate/ ├── lib/ # Utility functions │ ├── gemini.ts # Gemini integration ├── types/ # TypeScript types -├── public/ # Static assets \ No newline at end of file +├── public/ # Static assets +``` diff --git a/app/api/build/route.ts b/app/api/build/route.ts index 3b4fd49..d5845cb 100644 --- a/app/api/build/route.ts +++ b/app/api/build/route.ts @@ -1,4 +1,5 @@ import { NextResponse } from "next/server"; + import { generatePCBuildRecommendation } from "@/lib/gemini"; export async function POST(req: Request) { @@ -11,10 +12,10 @@ export async function POST(req: Request) { const requestData: PCBuilderRequest = await req.json(); if ( - !requestData.budget || - !requestData.currency || - !requestData.location || - !requestData.usages + !requestData.budget + || !requestData.currency + || !requestData.location + || !requestData.usages ) { return NextResponse.json({ error: "Missing required fields", @@ -24,10 +25,11 @@ export async function POST(req: Request) { const recommendation = await generatePCBuildRecommendation( requestData, - apiKey + apiKey, ); return NextResponse.json(recommendation); - } catch (error) { + } + catch (error) { console.error("PC Build API error:", error); return NextResponse.json({ error: error instanceof Error ? error.message : "Unknown error occurred", diff --git a/app/builder/builder.tsx b/app/builder/builder.tsx index d24a124..7e4d1b4 100644 --- a/app/builder/builder.tsx +++ b/app/builder/builder.tsx @@ -1,185 +1,184 @@ -"use client" +"use client"; - -import { motion, AnimatePresence } from "framer-motion"; -import { Progress } from "@/components/ui/progress"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; +import { AnimatePresence, motion } from "framer-motion"; import { useEffect, useState } from "react"; -import { usePCBuilder } from "@/hooks/usePCBuilder"; + import BuilderForm from "@/components/builder-form"; import BuilderResult from "@/components/builder-result"; import { ErrorDisplay } from "@/components/error-display"; +import { Badge } from "@/components/ui/badge"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Progress } from "@/components/ui/progress"; import { useTranslation } from "@/context/i18n-store"; - - - -const getPerformanceLabel = (rating: number) => { - if (rating >= 9) return 'Excellent'; - if (rating >= 7) return 'Very Good'; - if (rating >= 5) return 'Good'; - return 'Average'; -}; +import { usePCBuilder } from "@/hooks/use-pc-builder"; + +function getPerformanceLabel(rating: number) { + if (rating >= 9) + return "Excellent"; + if (rating >= 7) + return "Very Good"; + if (rating >= 5) + return "Good"; + return "Average"; +} export default function PCBuilder() { - - - const [step, setStep] = useState(1); - const [progress, setProgress] = useState(0); - const { recommendation, isLoading, generateBuild, reset, error } = usePCBuilder() - const [currency, setCurrency] = useState('') - const { t } = useTranslation(); - - - const handleReset = () => { - setStep(1); - setProgress(0); - reset() + const [step, setStep] = useState(1); + const [progress, setProgress] = useState(0); + const { recommendation, isLoading, generateBuild, reset, error } = usePCBuilder(); + const [currency, setCurrency] = useState(""); + const { t } = useTranslation(); + + const handleReset = () => { + setStep(1); + setProgress(0); + reset(); + }; + + useEffect(() => { + if (isLoading) { + const interval = setInterval(() => { + setProgress((prev) => { + if (prev >= 90) { + return prev; + } + return prev + 10; + }); + }, 300); + + return () => clearInterval(interval); } - - useEffect(() => { - if (isLoading) { - const interval = setInterval(() => { - setProgress(prev => { - if (prev >= 90) { - return prev; - } - return prev + 10; - }); - }, 300); - - return () => clearInterval(interval); - } else if (recommendation) { - setProgress(100); - } - }, [isLoading, recommendation]); - - return ( -
-
- -

{t('builder_page.title')}

-

- {t('builder_page.description')} -

-
- - {error ? ( - - ) : - - {step === 1 && ( - - - {t('builder_page.form.title')} - - - - - - )} - - {step === 2 && !recommendation && ( - - - - - - - - - - - - - - -

{t('builder_page.analyzing.title')}

-

- {t('builder_page.analyzing.description')} -

- -
- -
- 0% - {progress}% - 100% -
-
-
- )} - - - - {recommendation && ( - - -
- {t('builder_page.results.title')} - - {getPerformanceLabel(recommendation.performanceRating)} - -
-
- - - -
- )} -
-
- } - - - -

{t('builder_page.results.description')}

-

{t('builder_page.results.update_note')}

-
-
-
- ); -} \ No newline at end of file + }, [isLoading, recommendation]); + + return ( +
+
+ +

{t("builder_page.title")}

+

+ {t("builder_page.description")} +

+
+ + {error + ? ( + + ) + : ( + + + {step === 1 && ( + + + {t("builder_page.form.title")} + + + + + + )} + + {step === 2 && !recommendation && ( + + + + + + + + + + + + + + +

{t("builder_page.analyzing.title")}

+

+ {t("builder_page.analyzing.description")} +

+ +
+ +
+ 0% + + {progress} + % + + 100% +
+
+
+ )} + + {recommendation && ( + + +
+ {t("builder_page.results.title")} + + {getPerformanceLabel(recommendation.performanceRating)} + +
+
+ + + +
+ )} +
+
+ )} + + +

{t("builder_page.results.description")}

+

{t("builder_page.results.update_note")}

+
+
+
+ ); +} diff --git a/app/builder/page.tsx b/app/builder/page.tsx index a3df94f..a0bf499 100644 --- a/app/builder/page.tsx +++ b/app/builder/page.tsx @@ -1,60 +1,61 @@ -import React from 'react' -import BuilderPage from '@/app/builder/builder' -import { Metadata } from 'next'; +import type { Metadata } from "next"; + +import React from "react"; + +import BuilderPage from "@/app/builder/builder"; export const metadata: Metadata = { - title: 'Build Your Custom PC – PCMate', + title: "Build Your Custom PC – PCMate", description: - 'Choose your budget, location, and usage type — PCMate will suggest the perfect custom PC build for you, optimized for performance and price.', + "Choose your budget, location, and usage type — PCMate will suggest the perfect custom PC build for you, optimized for performance and price.", keywords: [ - 'custom PC builder', - 'budget PC build', - 'gaming PC Bangladesh', - 'best PC under budget', - 'PC configuration', - 'build your own PC', - 'AI PC recommendation', - 'computer builder', - 'pc build tool', - 'PCMate', + "custom PC builder", + "budget PC build", + "gaming PC Bangladesh", + "best PC under budget", + "PC configuration", + "build your own PC", + "AI PC recommendation", + "computer builder", + "pc build tool", + "PCMate", ], - metadataBase: new URL('https://yourdomain.com'), + metadataBase: new URL("https://yourdomain.com"), openGraph: { - title: 'Build Your Custom PC – PCMate', + title: "Build Your Custom PC – PCMate", description: - 'Smart PC builder that gives you the best build for your needs, whether for gaming, editing, or everyday use.', - url: 'https://yourdomain.com/builder', - siteName: 'PCMate', + "Smart PC builder that gives you the best build for your needs, whether for gaming, editing, or everyday use.", + url: "https://yourdomain.com/builder", + siteName: "PCMate", images: [ { - url: 'https://yourdomain.com/images/builder-preview.png', + url: "https://yourdomain.com/images/builder-preview.png", width: 1200, height: 630, - alt: 'PC Builder Preview', + alt: "PC Builder Preview", }, ], - locale: 'en_US', - type: 'website', + locale: "en_US", + type: "website", }, twitter: { - card: 'summary_large_image', - title: 'Build the Best PC with PCMate', + card: "summary_large_image", + title: "Build the Best PC with PCMate", description: - 'Tell us what you need — we’ll create the perfect PC build for your budget and location.', - images: ['https://yourdomain.com/images/builder-preview.png'], - creator: '@yourTwitterHandle', + "Tell us what you need — we’ll create the perfect PC build for your budget and location.", + images: ["https://yourdomain.com/images/builder-preview.png"], + creator: "@yourTwitterHandle", }, - authors: [{ name: 'Md Razu Islam', url: 'https://razuislam.vercel.app/' }], - creator: 'PCMate Team', - publisher: 'PCMate', - category: 'technology', + authors: [{ name: "Md Razu Islam", url: "https://razuislam.vercel.app/" }], + creator: "PCMate Team", + publisher: "PCMate", + category: "technology", }; - -const Page = () => { +function Page() { return ( - ) + ); } -export default Page \ No newline at end of file +export default Page; diff --git a/app/globals.css b/app/globals.css index 312fea7..29b6e63 100644 --- a/app/globals.css +++ b/app/globals.css @@ -87,7 +87,7 @@ --text-overline-letter-spacing: 0.1em; --text-overline-text-transform: uppercase; } - + :root { /* -- Semantic Colors -- */ --background: #fcfcfd; @@ -121,7 +121,7 @@ } .dark { - /* Dark Mode Colors */ + /* Dark Mode Colors */ --background: #09090b; --foreground: #fcfcfd; diff --git a/app/layout.tsx b/app/layout.tsx index dd240f2..fb53769 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,21 +1,23 @@ import type { Metadata } from "next"; -import { Inter, Noto_Sans } from "next/font/google"; + +import { ThemeProvider } from "next-themes"; + import "./globals.css"; +import { Inter, Noto_Sans } from "next/font/google"; + import Header from "@/components/header"; -import { ThemeProvider } from 'next-themes' import { TranslationProvider } from "@/context/i18n-store"; - const inter = Inter({ - subsets: ['latin'], - variable: '--font-inter', - display: 'swap', + subsets: ["latin"], + variable: "--font-inter", + display: "swap", }); const noto_sans = Noto_Sans({ - subsets: ['latin'], - variable: '--font-noto-sans', - display: 'swap', + subsets: ["latin"], + variable: "--font-noto-sans", + display: "swap", }); export const metadata: Metadata = { @@ -38,7 +40,11 @@ export default function RootLayout({
{children}
- © {new Date().getFullYear()} PCMate. All rights reserved. + © + {" "} + {new Date().getFullYear()} + {" "} + PCMate. All rights reserved.
diff --git a/app/page.tsx b/app/page.tsx index 5e704eb..ee50c05 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,69 +1,69 @@ +import type { Metadata } from "next"; import FeaturesSection from "@/components/featured"; import HeroSection from "@/components/hero-section"; import HowItWorks from "@/components/how-it-works"; import Testimonials from "@/components/testimonial"; -import { Metadata } from 'next'; export const metadata: Metadata = { - title: 'PCMate – Best PC & Laptop Builder for Your Budget', + title: "PCMate – Best PC & Laptop Builder for Your Budget", description: - 'Tell us your budget, location, and use-case — we’ll suggest the best PC build or laptop for you. Smart, fast and fully personalized recommendations.', + "Tell us your budget, location, and use-case — we’ll suggest the best PC build or laptop for you. Smart, fast and fully personalized recommendations.", keywords: [ - 'PC builder', - 'Laptop recommendation', - 'Budget PC guide', - 'Build your own PC', - 'Bangladesh PC builder', - 'Custom PC suggestion', - 'Laptop buying guide', - 'PCMate', - 'AI PC recommendation', + "PC builder", + "Laptop recommendation", + "Budget PC guide", + "Build your own PC", + "Bangladesh PC builder", + "Custom PC suggestion", + "Laptop buying guide", + "PCMate", + "AI PC recommendation", ], - metadataBase: new URL('https://yourdomain.com'), + metadataBase: new URL("https://yourdomain.com"), openGraph: { - title: 'PCMate – Best PC & Laptop Builder', + title: "PCMate – Best PC & Laptop Builder", description: - 'Instant PC or Laptop recommendations based on your budget, location, and needs.', - url: 'https://yourdomain.com', - siteName: 'PCMate', + "Instant PC or Laptop recommendations based on your budget, location, and needs.", + url: "https://yourdomain.com", + siteName: "PCMate", images: [ { - url: 'https://yourdomain.com/og-image.png', + url: "https://yourdomain.com/og-image.png", width: 1200, height: 630, - alt: 'PCMate preview', + alt: "PCMate preview", }, ], - locale: 'en_US', - type: 'website', + locale: "en_US", + type: "website", }, twitter: { - card: 'summary_large_image', - title: 'PCMate – Build or Buy the Right PC', + card: "summary_large_image", + title: "PCMate – Build or Buy the Right PC", description: - 'Smart guide to find the best PC build or laptop based on your specific use-case and budget.', - images: ['https://yourdomain.com/og-image.png'], - creator: '@yourTwitterHandle', + "Smart guide to find the best PC build or laptop based on your specific use-case and budget.", + images: ["https://yourdomain.com/og-image.png"], + creator: "@yourTwitterHandle", }, icons: { - icon: '/favicon.ico', - shortcut: '/favicon.ico', - apple: '/apple-touch-icon.png', + icon: "/favicon.ico", + shortcut: "/favicon.ico", + apple: "/apple-touch-icon.png", }, - authors: [{ name: 'Md Razu Islam', url: 'https://razuislam.vercel.app/' }], - creator: 'PCMate Team', - publisher: 'PCMate', - category: 'technology', + authors: [{ name: "Md Razu Islam", url: "https://razuislam.vercel.app/" }], + creator: "PCMate Team", + publisher: "PCMate", + category: "technology", }; export default function LandingPage() { return (
- - - - + + + +
); -} \ No newline at end of file +} diff --git a/components.json b/components.json index 335484f..4ee62ee 100644 --- a/components.json +++ b/components.json @@ -18,4 +18,4 @@ "hooks": "@/hooks" }, "iconLibrary": "lucide" -} \ No newline at end of file +} diff --git a/components/builder-form.tsx b/components/builder-form.tsx index b0fd3ee..b247aa7 100644 --- a/components/builder-form.tsx +++ b/components/builder-form.tsx @@ -1,267 +1,255 @@ -import React from 'react' +import type * as z from "zod"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { motion } from "framer-motion"; +import React from "react"; +import { useForm } from "react-hook-form"; + +import { Button } from "@/components/ui/button"; import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, } from "@/components/ui/select"; -import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; -import { Button } from "@/components/ui/button"; -import * as z from "zod"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useForm } from "react-hook-form"; -import { motion } from "framer-motion"; -import { useTranslation } from '@/context/i18n-store'; - - - - -export const formSchema = z.object({ - budget: z.number().min(1, "Budget is required!"), - currency: z.enum(["BDT", "USD", "EUR", "GBP"]), - location: z.string().min(2, "Location is required"), - usage: z.array(z.string()).min(1, "Select at least one use case"), - buildType: z.enum(["pc", "laptop", "both"]), - notes: z.string().max(200).optional(), -}); +import { useTranslation } from "@/context/i18n-store"; +import { formSchema } from "@/validation"; const usageOptions = [ - { id: "gaming", label: "Gaming", icon: "🎮", color: "bg-purple-100 text-purple-800" }, - { id: "design", label: "Design", icon: "🎨", color: "bg-blue-100 text-blue-800" }, - { id: "coding", label: "Coding", icon: "💻", color: "bg-green-100 text-green-800" }, - { id: "office", label: "Office", icon: "📊", color: "bg-yellow-100 text-yellow-800" }, - { id: "video", label: "Video Editing", icon: "🎥", color: "bg-red-100 text-red-800" }, - { id: "streaming", label: "Streaming", icon: "📡", color: "bg-indigo-100 text-indigo-800" }, + { id: "gaming", label: "Gaming", icon: "🎮", color: "bg-purple-100 text-purple-800" }, + { id: "design", label: "Design", icon: "🎨", color: "bg-blue-100 text-blue-800" }, + { id: "coding", label: "Coding", icon: "💻", color: "bg-green-100 text-green-800" }, + { id: "office", label: "Office", icon: "📊", color: "bg-yellow-100 text-yellow-800" }, + { id: "video", label: "Video Editing", icon: "🎥", color: "bg-red-100 text-red-800" }, + { id: "streaming", label: "Streaming", icon: "📡", color: "bg-indigo-100 text-indigo-800" }, ]; -interface BuildFormProps { - setStep: React.Dispatch> - setProgress: React.Dispatch> - generateBuild: (request: PCBuilderRequest) => Promise - isLoading: boolean - setCurrency: React.Dispatch> -} - -const BuilderForm = ({ - setProgress, - setStep, - generateBuild, - isLoading, - setCurrency -}: BuildFormProps) => { - - const { t, language } = useTranslation(); - - - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - budget: 1500, - currency: "BDT", - location: "", - usage: [], - buildType: "pc", - notes: "", - }, +type BuildFormProps = { + setStep: React.Dispatch>; + setProgress: React.Dispatch>; + generateBuild: (request: PCBuilderRequest) => Promise; + isLoading: boolean; + setCurrency: React.Dispatch>; +}; + +function BuilderForm({ + setProgress, + setStep, + generateBuild, + isLoading, + setCurrency, +}: BuildFormProps) { + const { t, language } = useTranslation(); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + budget: 1500, + currency: "BDT", + location: "", + usage: [], + buildType: "pc", + notes: "", + }, + }); + + const onSubmit = async (values: z.infer) => { + setProgress(0); + setStep(2); + + await generateBuild({ + budget: Number(values.budget), + currency: values.currency, + location: values.location, + usages: values.usage as ("gaming" | "video-editing" | "3d-rendering" | "development" | "office" | "general")[], + additionalNotes: values.notes, + preference: values.buildType as "pc" | "laptop" | "both", + language, }); - - - const onSubmit = async (values: z.infer) => { - setProgress(0); - setStep(2); - - await generateBuild({ - budget: Number(values.budget), - currency: values.currency, - location: values.location, - usages: values.usage as ('gaming' | 'video-editing' | '3d-rendering' | 'development' | 'office' | 'general')[], - additionalNotes: values.notes, - preference: values.buildType as 'pc' | 'laptop' | 'both', - language - }); - - setCurrency(values.currency) - form.reset() - - }; - - return ( - <> -
- -
- ( - - {t('builder_page.form.currency.label')} - - - - )} - /> - - ( - - {t('builder_page.form.budget.label')} - - field.onChange(parseInt(e.target.value))} - /> - - - - )} - /> -
- - ( - - {t('builder_page.form.location.label')} - - - - - - )} - /> - -
- {t('builder_page.form.usage.label')} -
- {usageOptions.map(option => ( - - - - ))} -
- {form.formState.errors.usage && ( -

- {form.formState.errors.usage.message} -

- )} -
- - ( - - {t('builder_page.form.build_type.label')} - - - - )} + setCurrency(values.currency); + form.reset(); + }; + + return ( + <> + + +
+ ( + + {t("builder_page.form.currency.label")} + + + + )} + /> + + ( + + {t("builder_page.form.budget.label")} + + field.onChange(Number.parseInt(e.target.value))} /> - - ( - - {t('builder_page.form.notes.label')} - -