diff --git a/.idx/icon.png b/.idx/icon.png new file mode 100644 index 0000000..8860a87 Binary files /dev/null and b/.idx/icon.png differ diff --git a/.modified b/.modified new file mode 100644 index 0000000..e69de29 diff --git a/docs/blueprint.md b/docs/blueprint.md new file mode 100644 index 0000000..7455a2a --- /dev/null +++ b/docs/blueprint.md @@ -0,0 +1,21 @@ +# **App Name**: EstimateFlow + +## Core Features: + +- Cost Input Form: Interactive form for clients to input project requirements. +- Admin Configuration: Admin panel for configuring pricing parameters and viewing client inputs. +- WordPress Integration: Ability to embed the cost calculator form directly into WordPress websites. +- AI-Assisted Scope Definition: AI-powered suggestions to guide clients in defining project scope and features by acting like a 'tool' offering project management advise +- Dynamic Cost Calculation: Real-time display of cost estimates based on input parameters. +- Custom Calculation Formulas: Options for agencies to customize the cost calculation formula via the admin panel +- AI Complexity Classification: Automatically classify feature complexity from project descriptions by using a 'tool' powered by LLMs, categorizing them as simple, medium, or complex to refine cost estimates + +## Style Guidelines: + +- Primary color: Strong blue (#2979FF) for a modern and trustworthy feel, drawing inspiration from technology and professionalism. +- Background color: Light grayish-blue (#E0E7FF) to provide a clean, unobtrusive backdrop that complements the primary color. +- Accent color: Purple (#794BC4), an analogous color to blue, to add a touch of sophistication and highlight key actions. +- Font Pairing: 'Space Grotesk' (sans-serif) for headings and 'Inter' (sans-serif) for body text. +- Clean, intuitive layout with a focus on user experience. Form elements should be logically grouped and easy to understand. Model initial layout after provided screenshot. +- Use simple, geometric icons to represent different features and project types. +- Subtle animations to provide feedback and guide the user through the cost estimation process. For instance, highlighting a section of the form when it becomes active, and adding transitions between each step of the process \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 55b1a36..357defa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,7 @@ "dotenv": "^16.5.0", "embla-carousel-react": "^8.6.0", "firebase": "^11.9.1", + "framer-motion": "^11.3.12", "genkit": "^1.14.1", "lucide-react": "^0.475.0", "next": "15.3.3", @@ -6213,6 +6214,33 @@ "node": ">= 0.6" } }, + "node_modules/framer-motion": { + "version": "11.18.2", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.2.tgz", + "integrity": "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==", + "license": "MIT", + "dependencies": { + "motion-dom": "^11.18.1", + "motion-utils": "^11.18.1", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -7537,6 +7565,21 @@ "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==" }, + "node_modules/motion-dom": { + "version": "11.18.1", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz", + "integrity": "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==", + "license": "MIT", + "dependencies": { + "motion-utils": "^11.18.1" + } + }, + "node_modules/motion-utils": { + "version": "11.18.1", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.18.1.tgz", + "integrity": "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", diff --git a/package.json b/package.json index ec4e4cb..07387b6 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "dotenv": "^16.5.0", "embla-carousel-react": "^8.6.0", "firebase": "^11.9.1", + "framer-motion": "^11.3.12", "genkit": "^1.14.1", "lucide-react": "^0.475.0", "next": "15.3.3", diff --git a/src/ai/dev.ts b/src/ai/dev.ts index 51e556a..f4b4886 100644 --- a/src/ai/dev.ts +++ b/src/ai/dev.ts @@ -1 +1,5 @@ -// Flows will be imported for their side effects in this file. +import { config } from 'dotenv'; +config(); + +import '@/ai/flows/suggest-features.ts'; +import '@/ai/flows/classify-complexity.ts'; \ No newline at end of file diff --git a/src/ai/flows/classify-complexity.ts b/src/ai/flows/classify-complexity.ts new file mode 100644 index 0000000..98d964d --- /dev/null +++ b/src/ai/flows/classify-complexity.ts @@ -0,0 +1,52 @@ +// This is an autogenerated file from Firebase Studio. +'use server'; + +/** + * @fileOverview This file defines a Genkit flow for classifying the complexity of project features. + * + * - classifyComplexity - An async function that takes a feature description and returns its complexity classification. + * - ClassifyComplexityInput - The input type for the classifyComplexity function. + * - ClassifyComplexityOutput - The output type for the classifyComplexity function. + */ + +import {ai} from '@/ai/genkit'; +import {z} from 'genkit'; + +const ClassifyComplexityInputSchema = z.object({ + featureDescription: z + .string() + .describe('A detailed description of the feature to be classified.'), +}); +export type ClassifyComplexityInput = z.infer; + +const ClassifyComplexityOutputSchema = z.object({ + complexity: z + .enum(['simple', 'medium', 'complex']) + .describe('The complexity classification of the feature.'), +}); +export type ClassifyComplexityOutput = z.infer; + +export async function classifyComplexity( + input: ClassifyComplexityInput +): Promise { + return classifyComplexityFlow(input); +} + +const prompt = ai.definePrompt({ + name: 'classifyComplexityPrompt', + input: {schema: ClassifyComplexityInputSchema}, + output: {schema: ClassifyComplexityOutputSchema}, + prompt: `You are an expert project manager classifying the complexity of software features. Classify the following feature description as either 'simple', 'medium', or 'complex'.\n\nFeature Description: {{{featureDescription}}}\n\nComplexity:`, +}); + +const classifyComplexityFlow = ai.defineFlow( + { + name: 'classifyComplexityFlow', + inputSchema: ClassifyComplexityInputSchema, + outputSchema: ClassifyComplexityOutputSchema, + }, + async input => { + const {output} = await prompt(input); + return output!; + } +); diff --git a/src/ai/flows/suggest-features.ts b/src/ai/flows/suggest-features.ts new file mode 100644 index 0000000..f09fbfc --- /dev/null +++ b/src/ai/flows/suggest-features.ts @@ -0,0 +1,59 @@ +'use server'; + +/** + * @fileOverview AI-powered feature suggestion flow based on project description. + * + * - suggestFeatures - A function that suggests features for a project based on its description. + * - SuggestFeaturesInput - The input type for the suggestFeatures function. + * - SuggestFeaturesOutput - The return type for the suggestFeatures function. + */ + +import {ai} from '@/ai/genkit'; +import {z} from 'genkit'; + +const SuggestFeaturesInputSchema = z.object({ + projectDescription: z + .string() + .describe('A brief description of the digital product or project.'), +}); +export type SuggestFeaturesInput = z.infer; + +const SuggestFeaturesOutputSchema = z.object({ + suggestedFeatures: z + .array(z.string()) + .describe('An array of suggested features for the project.'), + scopeDefinitions: z + .string() + .describe('A paragraph defining the overall scope of the project.'), +}); +export type SuggestFeaturesOutput = z.infer; + +export async function suggestFeatures(input: SuggestFeaturesInput): Promise { + return suggestFeaturesFlow(input); +} + +const prompt = ai.definePrompt({ + name: 'suggestFeaturesPrompt', + input: {schema: SuggestFeaturesInputSchema}, + output: {schema: SuggestFeaturesOutputSchema}, + prompt: `You are an AI assistant helping to define the scope and features of digital projects. + + Based on the following project description, suggest a list of potential features and a paragraph defining the project's scope. + + Project Description: {{{projectDescription}}} + + Format the features as a bulleted list and provide a concise scope definition. + `, +}); + +const suggestFeaturesFlow = ai.defineFlow( + { + name: 'suggestFeaturesFlow', + inputSchema: SuggestFeaturesInputSchema, + outputSchema: SuggestFeaturesOutputSchema, + }, + async input => { + const {output} = await prompt(input); + return output!; + } +); diff --git a/src/app/globals.css b/src/app/globals.css index a8144b6..f3e27f8 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2,79 +2,50 @@ @tailwind components; @tailwind utilities; -body { - font-family: Arial, Helvetica, sans-serif; -} - @layer base { :root { - --background: 0 0% 100%; - --foreground: 0 0% 3.9%; + --background: 226 100% 94.1%; + --foreground: 222 84% 4.9%; --card: 0 0% 100%; - --card-foreground: 0 0% 3.9%; + --card-foreground: 222 84% 4.9%; --popover: 0 0% 100%; - --popover-foreground: 0 0% 3.9%; - --primary: 0 0% 9%; + --popover-foreground: 222 84% 4.9%; + --primary: 221 100% 58%; --primary-foreground: 0 0% 98%; - --secondary: 0 0% 96.1%; - --secondary-foreground: 0 0% 9%; - --muted: 0 0% 96.1%; - --muted-foreground: 0 0% 45.1%; - --accent: 0 0% 96.1%; - --accent-foreground: 0 0% 9%; + --secondary: 226 40% 90%; + --secondary-foreground: 222 84% 4.9%; + --muted: 226 40% 90%; + --muted-foreground: 222 20% 46.1%; + --accent: 263 47% 53%; + --accent-foreground: 0 0% 98%; --destructive: 0 84.2% 60.2%; --destructive-foreground: 0 0% 98%; - --border: 0 0% 89.8%; - --input: 0 0% 89.8%; - --ring: 0 0% 3.9%; - --chart-1: 12 76% 61%; - --chart-2: 173 58% 39%; - --chart-3: 197 37% 24%; - --chart-4: 43 74% 66%; - --chart-5: 27 87% 67%; + --border: 226 30% 85%; + --input: 226 30% 85%; + --ring: 221 100% 58%; --radius: 0.5rem; - --sidebar-background: 0 0% 98%; - --sidebar-foreground: 240 5.3% 26.1%; - --sidebar-primary: 240 5.9% 10%; - --sidebar-primary-foreground: 0 0% 98%; - --sidebar-accent: 240 4.8% 95.9%; - --sidebar-accent-foreground: 240 5.9% 10%; - --sidebar-border: 220 13% 91%; - --sidebar-ring: 217.2 91.2% 59.8%; } + .dark { - --background: 0 0% 3.9%; + --background: 222 47% 11%; --foreground: 0 0% 98%; - --card: 0 0% 3.9%; + --card: 222 47% 11%; --card-foreground: 0 0% 98%; - --popover: 0 0% 3.9%; + --popover: 222 47% 11%; --popover-foreground: 0 0% 98%; - --primary: 0 0% 98%; + --primary: 221 100% 58%; --primary-foreground: 0 0% 9%; - --secondary: 0 0% 14.9%; + --secondary: 222 25% 20%; --secondary-foreground: 0 0% 98%; - --muted: 0 0% 14.9%; - --muted-foreground: 0 0% 63.9%; - --accent: 0 0% 14.9%; + --muted: 222 25% 20%; + --muted-foreground: 222 10% 63.9%; + --accent: 263 47% 53%; --accent-foreground: 0 0% 98%; --destructive: 0 62.8% 30.6%; --destructive-foreground: 0 0% 98%; - --border: 0 0% 14.9%; - --input: 0 0% 14.9%; - --ring: 0 0% 83.1%; - --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; - --sidebar-background: 240 5.9% 10%; - --sidebar-foreground: 240 4.8% 95.9%; - --sidebar-primary: 224.3 76.3% 48%; - --sidebar-primary-foreground: 0 0% 100%; - --sidebar-accent: 240 3.7% 15.9%; - --sidebar-accent-foreground: 240 4.8% 95.9%; - --sidebar-border: 240 3.7% 15.9%; - --sidebar-ring: 217.2 91.2% 59.8%; + --border: 222 25% 20%; + --input: 222 25% 20%; + --ring: 221 100% 58%; } } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index c81ce2d..c296029 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,9 +1,10 @@ import type {Metadata} from 'next'; import './globals.css'; +import { Toaster } from "@/components/ui/toaster"; export const metadata: Metadata = { - title: 'Firebase Studio App', - description: 'Generated by Firebase Studio', + title: 'EstimateFlow', + description: 'Estimate development costs for your next digital product.', }; export default function RootLayout({ @@ -14,11 +15,14 @@ export default function RootLayout({ return ( - - - + + + - {children} + + {children} + + ); } diff --git a/src/app/page.tsx b/src/app/page.tsx index 6ff5373..e0b8bcb 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,3 +1,17 @@ +import { CostEstimatorForm } from '@/components/cost-estimator/cost-estimator-form'; + export default function Home() { - return <>; + return ( +
+
+

+ EstimateFlow +

+

+ Answer a few questions to get a real-time estimate for your digital project. Let's build something amazing together. +

+
+ +
+ ); } diff --git a/src/components/cost-estimator/cost-estimator-form.tsx b/src/components/cost-estimator/cost-estimator-form.tsx new file mode 100644 index 0000000..9c43a7b --- /dev/null +++ b/src/components/cost-estimator/cost-estimator-form.tsx @@ -0,0 +1,74 @@ +"use client"; + +import React, { useState, useTransition } from 'react'; +import { Step1ProjectType } from './step-1-project-type'; +import { Card, CardContent } from '@/components/ui/card'; +import { AnimatePresence, motion } from 'framer-motion'; + +export type FormData = { + projectType: 'website' | 'mobile-app' | 'platform' | null; + // Add more fields for subsequent steps +}; + +export function CostEstimatorForm() { + const [currentStep, setCurrentStep] = useState(1); + const [formData, setFormData] = useState({ + projectType: null, + }); + const [isPending, startTransition] = useTransition(); + + const handleNextStep = () => { + startTransition(() => { + setCurrentStep((prev) => prev + 1); + }); + }; + + const handleUpdateFormData = (newData: Partial) => { + setFormData((prev) => ({ ...prev, ...newData })); + }; + + const renderStep = () => { + switch (currentStep) { + case 1: + return ( + + ); + case 2: + return ( +
+

Step 2: Define Your Project

+

This is where AI will help you define scope.

+

You selected: {formData.projectType}

+
+ ) + default: + return ( + + ); + } + }; + + return ( + + + + + {renderStep()} + + + + + ); +} diff --git a/src/components/cost-estimator/step-1-project-type.tsx b/src/components/cost-estimator/step-1-project-type.tsx new file mode 100644 index 0000000..9e4e1ff --- /dev/null +++ b/src/components/cost-estimator/step-1-project-type.tsx @@ -0,0 +1,69 @@ +"use client"; + +import type { FormData } from './cost-estimator-form'; +import { Monitor, Smartphone, Layers } from 'lucide-react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import React from 'react'; + +type Step1Props = { + onNext: () => void; + onUpdateData: (data: Partial) => void; +}; + +const projectTypes = [ + { + id: 'website', + title: 'Website', + description: 'A responsive and engaging online presence.', + icon: , + }, + { + id: 'mobile-app', + title: 'Mobile App', + description: 'An iOS or Android application for users on the go.', + icon: , + }, + { + id: 'platform', + title: 'Platform', + description: 'A complex system with multiple user types.', + icon: , + }, +]; + +export function Step1ProjectType({ onNext, onUpdateData }: Step1Props) { + const handleSelect = (projectType: 'website' | 'mobile-app' | 'platform') => { + onUpdateData({ projectType }); + onNext(); + }; + + return ( +
+

What are you looking to build?

+

Select a project type to get started.

+
+ {projectTypes.map((type) => ( + handleSelect(type.id as any)} + className="cursor-pointer transition-all duration-300 ease-in-out hover:shadow-accent/20 hover:shadow-lg hover:-translate-y-1 hover:border-primary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + handleSelect(type.id as any); + } + }} + > + +
{type.icon}
+ {type.title} +
+ +

{type.description}

+
+
+ ))} +
+
+ ); +} diff --git a/tailwind.config.ts b/tailwind.config.ts index 4d4a68f..489523e 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -11,7 +11,7 @@ export default { extend: { fontFamily: { body: ['Inter', 'sans-serif'], - headline: ['Inter', 'sans-serif'], + headline: ['"Space Grotesk"', 'sans-serif'], code: ['monospace'], }, colors: {