Updated app

This commit is contained in:
Leon Serfaty G
2025-07-17 10:06:15 +00:00
parent 771048d9f8
commit 4b7d3b2b3d
14 changed files with 376 additions and 64 deletions
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File
+21
View File
@@ -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
+43
View File
@@ -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",
+1
View File
@@ -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",
+5 -1
View File
@@ -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';
+52
View File
@@ -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<typeof ClassifyComplexityInputSchema>;
const ClassifyComplexityOutputSchema = z.object({
complexity: z
.enum(['simple', 'medium', 'complex'])
.describe('The complexity classification of the feature.'),
});
export type ClassifyComplexityOutput = z.infer<typeof ClassifyComplexityOutputSchema>;
export async function classifyComplexity(
input: ClassifyComplexityInput
): Promise<ClassifyComplexityOutput> {
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!;
}
);
+59
View File
@@ -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<typeof SuggestFeaturesInputSchema>;
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<typeof SuggestFeaturesOutputSchema>;
export async function suggestFeatures(input: SuggestFeaturesInput): Promise<SuggestFeaturesOutput> {
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!;
}
);
+26 -55
View File
@@ -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%;
}
}
+10 -6
View File
@@ -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 (
<html lang="en">
<head>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter&display=swap" rel="stylesheet"></link>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&family=Space+Grotesk:wght@400;500;700&display=swap" rel="stylesheet" />
</head>
<body className="font-body antialiased">{children}</body>
<body className="font-body antialiased">
{children}
<Toaster />
</body>
</html>
);
}
+15 -1
View File
@@ -1,3 +1,17 @@
import { CostEstimatorForm } from '@/components/cost-estimator/cost-estimator-form';
export default function Home() {
return <></>;
return (
<main className="flex min-h-screen w-full flex-col items-center justify-center bg-background p-4 sm:p-8 md:p-12 lg:p-24">
<div className="w-full max-w-4xl text-center">
<h1 className="font-headline text-4xl font-bold tracking-tighter text-foreground sm:text-5xl md:text-6xl">
EstimateFlow
</h1>
<p className="mx-auto mt-4 max-w-[700px] text-muted-foreground md:text-xl">
Answer a few questions to get a real-time estimate for your digital project. Let's build something amazing together.
</p>
</div>
<CostEstimatorForm />
</main>
);
}
@@ -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<FormData>({
projectType: null,
});
const [isPending, startTransition] = useTransition();
const handleNextStep = () => {
startTransition(() => {
setCurrentStep((prev) => prev + 1);
});
};
const handleUpdateFormData = (newData: Partial<FormData>) => {
setFormData((prev) => ({ ...prev, ...newData }));
};
const renderStep = () => {
switch (currentStep) {
case 1:
return (
<Step1ProjectType
onNext={handleNextStep}
onUpdateData={handleUpdateFormData}
/>
);
case 2:
return (
<div className="flex flex-col items-center text-center">
<h2 className="font-headline text-3xl font-bold tracking-tight">Step 2: Define Your Project</h2>
<p className="mt-2 text-muted-foreground">This is where AI will help you define scope.</p>
<p className="mt-4 text-sm font-medium">You selected: <span className="text-primary">{formData.projectType}</span></p>
</div>
)
default:
return (
<Step1ProjectType
onNext={handleNextStep}
onUpdateData={handleUpdateFormData}
/>
);
}
};
return (
<Card className="mt-8 w-full max-w-4xl shadow-2xl overflow-hidden">
<CardContent className="p-4 sm:p-8">
<AnimatePresence mode="wait">
<motion.div
key={currentStep}
initial={{ opacity: 0, x: 50 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -50 }}
transition={{ duration: 0.3 }}
>
{renderStep()}
</motion.div>
</AnimatePresence>
</CardContent>
</Card>
);
}
@@ -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<FormData>) => void;
};
const projectTypes = [
{
id: 'website',
title: 'Website',
description: 'A responsive and engaging online presence.',
icon: <Monitor className="h-8 w-8 text-primary" />,
},
{
id: 'mobile-app',
title: 'Mobile App',
description: 'An iOS or Android application for users on the go.',
icon: <Smartphone className="h-8 w-8 text-primary" />,
},
{
id: 'platform',
title: 'Platform',
description: 'A complex system with multiple user types.',
icon: <Layers className="h-8 w-8 text-primary" />,
},
];
export function Step1ProjectType({ onNext, onUpdateData }: Step1Props) {
const handleSelect = (projectType: 'website' | 'mobile-app' | 'platform') => {
onUpdateData({ projectType });
onNext();
};
return (
<div className="flex flex-col items-center text-center">
<h2 className="font-headline text-3xl font-bold tracking-tight">What are you looking to build?</h2>
<p className="mt-2 text-muted-foreground">Select a project type to get started.</p>
<div className="mt-8 grid w-full grid-cols-1 gap-6 md:grid-cols-3">
{projectTypes.map((type) => (
<Card
key={type.id}
onClick={() => 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);
}
}}
>
<CardHeader className="items-center">
<div className="rounded-full bg-primary/10 p-4 mb-4">{type.icon}</div>
<CardTitle className="font-headline text-xl">{type.title}</CardTitle>
</CardHeader>
<CardContent>
<p className="text-muted-foreground">{type.description}</p>
</CardContent>
</Card>
))}
</div>
</div>
);
}
+1 -1
View File
@@ -11,7 +11,7 @@ export default {
extend: {
fontFamily: {
body: ['Inter', 'sans-serif'],
headline: ['Inter', 'sans-serif'],
headline: ['"Space Grotesk"', 'sans-serif'],
code: ['monospace'],
},
colors: {