From 06fbbab24dc40f0aaee83f7f3b5b441105074482 Mon Sep 17 00:00:00 2001 From: Riccardo Senica Date: Sat, 25 Jan 2025 20:39:55 +0100 Subject: [PATCH] feat: first draft --- .eslintrc.json | 14 + .gitignore | 155 +- .prettierrc | 7 + README.md | 127 +- app/api/backup/route.ts | 15 + app/api/restore/route.ts | 35 + app/favicon.ico | Bin 0 -> 17276 bytes app/globals.css | 193 + app/layout.tsx | 45 + app/page.tsx | 47 + components.json | 21 + components/BackupControls.tsx | 133 + components/Header.tsx | 34 + components/ThemeProvider.tsx | 12 + components/ThemeToggle.tsx | 37 + components/core/FourActions.tsx | 124 + components/core/PriceCorridor.tsx | 301 ++ components/core/SixPaths.tsx | 156 + components/core/StrategyCanvas.tsx | 386 ++ components/core/UtilityMap.tsx | 97 + components/core/Validation.tsx | 131 + components/ui/alert.tsx | 59 + components/ui/button.tsx | 57 + components/ui/card.tsx | 83 + components/ui/checkbox.tsx | 30 + components/ui/dialog.tsx | 122 + components/ui/editable-list.tsx | 32 + components/ui/factor-input.tsx | 48 + components/ui/input.tsx | 22 + components/ui/label.tsx | 26 + components/ui/notes-field.tsx | 23 + components/ui/select.tsx | 159 + components/ui/slider.tsx | 28 + components/ui/tabs.tsx | 55 + components/ui/textarea.tsx | 22 + components/ui/toggle.tsx | 45 + components/ui/tool-card.tsx | 18 + contexts/state/StateContext.tsx | 68 + contexts/state/StateProvider.tsx | 86 + eslint.config.mjs | 16 + next.config.ts | 7 + package-lock.json | 5895 ++++++++++++++++++++++++++++ package.json | 54 + postcss.config.mjs | 8 + tailwind.config.ts | 66 + tsconfig.json | 29 + utils/cn.ts | 6 + utils/redis.ts | 18 + utils/stateReducer.ts | 149 + utils/types.ts | 52 + utils/useStorage.ts | 80 + utils/validateState.ts | 109 + yarn.lock | 3997 +++++++++++++++++++ 53 files changed, 13416 insertions(+), 123 deletions(-) create mode 100644 .eslintrc.json create mode 100644 .prettierrc create mode 100644 app/api/backup/route.ts create mode 100644 app/api/restore/route.ts create mode 100644 app/favicon.ico create mode 100644 app/globals.css create mode 100644 app/layout.tsx create mode 100644 app/page.tsx create mode 100644 components.json create mode 100644 components/BackupControls.tsx create mode 100644 components/Header.tsx create mode 100644 components/ThemeProvider.tsx create mode 100644 components/ThemeToggle.tsx create mode 100644 components/core/FourActions.tsx create mode 100644 components/core/PriceCorridor.tsx create mode 100644 components/core/SixPaths.tsx create mode 100644 components/core/StrategyCanvas.tsx create mode 100644 components/core/UtilityMap.tsx create mode 100644 components/core/Validation.tsx create mode 100644 components/ui/alert.tsx create mode 100644 components/ui/button.tsx create mode 100644 components/ui/card.tsx create mode 100644 components/ui/checkbox.tsx create mode 100644 components/ui/dialog.tsx create mode 100644 components/ui/editable-list.tsx create mode 100644 components/ui/factor-input.tsx create mode 100644 components/ui/input.tsx create mode 100644 components/ui/label.tsx create mode 100644 components/ui/notes-field.tsx create mode 100644 components/ui/select.tsx create mode 100644 components/ui/slider.tsx create mode 100644 components/ui/tabs.tsx create mode 100644 components/ui/textarea.tsx create mode 100644 components/ui/toggle.tsx create mode 100644 components/ui/tool-card.tsx create mode 100644 contexts/state/StateContext.tsx create mode 100644 contexts/state/StateProvider.tsx create mode 100644 eslint.config.mjs create mode 100644 next.config.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 postcss.config.mjs create mode 100644 tailwind.config.ts create mode 100644 tsconfig.json create mode 100644 utils/cn.ts create mode 100644 utils/redis.ts create mode 100644 utils/stateReducer.ts create mode 100644 utils/types.ts create mode 100644 utils/useStorage.ts create mode 100644 utils/validateState.ts create mode 100644 yarn.lock diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..c65d382 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,14 @@ +{ + "extends": [ + "next/core-web-vitals", + "plugin:@typescript-eslint/recommended", + "prettier" + ], + "plugins": ["@typescript-eslint", "prettier"], + "rules": { + "prettier/prettier": "error", + "@typescript-eslint/no-unused-vars": "error", + "@typescript-eslint/no-explicit-any": "error", + "react-hooks/exhaustive-deps": "warn" + } +} diff --git a/.gitignore b/.gitignore index c6bba59..5ef6a52 100644 --- a/.gitignore +++ b/.gitignore @@ -1,130 +1,41 @@ -# Logs -logs -*.log +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug npm-debug.log* yarn-debug.log* yarn-error.log* -lerna-debug.log* .pnpm-debug.log* -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json +# env files (can opt-in for committing if needed) +.env* -# Runtime data -pids -*.pid -*.seed -*.pid.lock +# vercel +.vercel -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# Snowpack dependency directory (https://snowpack.dev/) -web_modules/ - -# TypeScript cache +# typescript *.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional stylelint cache -.stylelintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variable files -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# parcel-bundler cache (https://parceljs.org/) -.cache -.parcel-cache - -# Next.js build output -.next -out - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# vuepress v2.x temp and cache directory -.temp -.cache - -# Docusaurus cache and generated files -.docusaurus - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test - -# yarn v2 -.yarn/cache -.yarn/unplugged -.yarn/build-state.yml -.yarn/install-state.gz -.pnp.* +next-env.d.ts diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..123d3e8 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "tabWidth": 2, + "useTabs": false +} diff --git a/README.md b/README.md index a98445a..1fcf509 100644 --- a/README.md +++ b/README.md @@ -1 +1,126 @@ -# blue-ocean \ No newline at end of file +# Blue Ocean Strategy Analysis Tool + +A comprehensive web application for visualizing and analyzing business strategies using the Blue Ocean Strategy framework. This tool helps strategists and business analysts create, validate, and visualize market-creating strategies through an interactive interface. + +## Features + +### ๐Ÿ’น Strategy Canvas + +- Interactive line chart visualization +- Factor management with market vs. idea comparison +- Notes and annotations for each factor +- Drag-and-drop factor reordering + +### ๐ŸŽฏ Four Actions Framework + +- Organize strategic factors into Eliminate/Reduce/Raise/Create +- Link factors with strategy canvas +- Notes per action category +- Color-coded sections for clarity + +### ๐Ÿ›ฃ๏ธ Six Paths Framework + +- Analysis across alternative industries +- Strategic group evaluation +- Buyer chain exploration +- Complementary products/services assessment +- Functional/emotional appeal analysis +- Time trend tracking + +### ๐Ÿ“Š Buyer Utility Map + +- Interactive grid visualization +- Toggle opportunities +- Notes per cell +- Six stages ร— six utility levers + +### ๐Ÿ’ฐ Price Corridor + +- Target price setting +- Competitor price tracking +- Visual price band analysis +- Three-tier market segmentation + +### โœ… Strategy Validation + +- Non-customer analysis +- Strategic sequence validation +- Implementation notes +- Progress tracking + +## Technical Stack + +- **Framework**: Next.js 14 with App Router +- **Language**: TypeScript 5 +- **Styling**: Tailwind CSS + shadcn/ui +- **Storage**: Redis +- **Charts**: Recharts +- **Utilities**: Lodash + +## Getting Started + +1. Clone the repository + +```bash +git clone https://github.com/riccardosenica/blue-ocean.git +cd blue-ocean +``` + +2. Install dependencies + +```bash +yarn install +``` + +3. Set up environment variables + +```bash +cp .env.example .env.local +``` + +4. Update the following variables in `.env.local`: + +``` +KV_REST_API_URL=your_kv_url +KV_REST_API_TOKEN=your_kv_token +``` + +5. Run the development server + +```bash +yarn dev +``` + +6. Open [http://localhost:3000](http://localhost:3000) in your browser + +## State Management + +The application uses React Context for state management with the following structure: + +- Strategy Canvas data +- Four Actions framework entries +- Six Paths analysis +- Utility Map toggles and notes +- Price Corridor data +- Validation checkpoints + +## Storage + +- Primary storage in browser's localStorage +- Backup functionality to Redis +- Automatic state persistence +- Import/Export capabilities + +## Development + +### Available Scripts + +- `yarn dev`: Start development server +- `yarn build`: Build for production +- `yarn start`: Start production server +- `yarn lint`: Run ESLint +- `yarn typecheck`: Run TypeScript compiler + +## Acknowledgments + +- Blue Ocean Strategy by W. Chan Kim and Renรฉe Mauborgne diff --git a/app/api/backup/route.ts b/app/api/backup/route.ts new file mode 100644 index 0000000..0148f25 --- /dev/null +++ b/app/api/backup/route.ts @@ -0,0 +1,15 @@ +import { redis } from '@utils/redis'; +import { NextResponse } from 'next/server'; +import crypto from 'crypto'; + +export async function POST(req: Request) { + try { + const { state } = await req.json(); + const key = crypto.randomBytes(8).toString('hex'); + await redis.set(key, JSON.stringify(state)); + return NextResponse.json({ success: true, key }); + } catch (error) { + console.error(error); + return NextResponse.json({ error: 'Backup failed' }, { status: 500 }); + } +} diff --git a/app/api/restore/route.ts b/app/api/restore/route.ts new file mode 100644 index 0000000..b63754f --- /dev/null +++ b/app/api/restore/route.ts @@ -0,0 +1,35 @@ +import { redis } from '@utils/redis'; +import { validateState } from '@utils/validateState'; +import { NextResponse } from 'next/server'; + +export async function GET(req: Request) { + try { + const { searchParams } = new URL(req.url); + const key = searchParams.get('key'); + + if (!key) { + return NextResponse.json({ error: 'No key provided' }, { status: 400 }); + } + + const state = await redis.get(key); + + if (!state) { + return NextResponse.json( + { error: 'No data found for this key' }, + { status: 404 } + ); + } + + const validatedState = validateState(state); + return NextResponse.json({ data: validatedState }); + } catch (error) { + console.error('Restore operation failed:', error); + return NextResponse.json( + { + error: 'Restore failed', + details: error instanceof Error ? error.message : 'Unknown error', + }, + { status: 500 } + ); + } +} diff --git a/app/favicon.ico b/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..7a01a76dbffe2f955a69bceac78e0c17d545d300 GIT binary patch literal 17276 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 + + + +
+
+
+
+
{children}
+
+
+
+
+
+ + + ); +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..430ca66 --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,47 @@ +'use client'; + +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@components/ui/tabs'; +import StrategyCanvas from '@components/core/StrategyCanvas'; +import FourActions from '@components/core/FourActions'; +import SixPaths from '@components/core/SixPaths'; +import UtilityMap from '@components/core/UtilityMap'; +import PriceCorridor from '@components/core/PriceCorridor'; +import Validation from '@components/core/Validation'; +import BackupControls from '@components/BackupControls'; + +export default function Home() { + return ( + <> + + + + Strategy Canvas + Four Actions + Six Paths + Utility Map + Price Corridor + Validation + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/components.json b/components.json new file mode 100644 index 0000000..444605f --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "app/globals.css", + "baseColor": "zinc", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/components/BackupControls.tsx b/components/BackupControls.tsx new file mode 100644 index 0000000..cf2d7fb --- /dev/null +++ b/components/BackupControls.tsx @@ -0,0 +1,133 @@ +import { useState } from 'react'; +import { Loader2, Copy, Check, RotateCcw } from 'lucide-react'; +import { Button } from '@components/ui/button'; +import { Input } from '@components/ui/input'; +import { Card, CardContent, CardHeader, CardTitle } from '@components/ui/card'; +import { Alert, AlertDescription } from '@components/ui/alert'; +import { useStorage } from '@utils/useStorage'; +import { useGlobalState } from '@contexts/state/StateContext'; + +export function LoadingSpinner() { + return ; +} + +export default function BackupControls() { + const [key, setKey] = useState(''); + const [backupKey, setBackupKey] = useState(''); + const [isBackingUp, setIsBackingUp] = useState(false); + const [isRestoring, setIsRestoring] = useState(false); + const [copied, setCopied] = useState(false); + const [error, setError] = useState(null); + const { state, dispatch, resetState } = useGlobalState(); + const { backupState, restoreState } = useStorage(); + + const handleBackup = async () => { + setIsBackingUp(true); + setError(null); + try { + const result = await backupState(state); + if (result?.key) { + setBackupKey(result.key); + } + } catch (error) { + console.error(error); + setError('Failed to backup data. Please try again.'); + } finally { + setIsBackingUp(false); + } + }; + + const handleRestore = async () => { + if (!key) { + setError('Please enter a key'); + return; + } + setIsRestoring(true); + setError(null); + try { + const restored = await restoreState(key); + if (restored) { + dispatch({ type: 'SET_STATE', payload: restored }); + setKey(''); + } else { + setError('No data found for this key'); + } + } catch (error) { + console.error(error); + setError('Failed to restore data. Please check your key and try again.'); + } finally { + setIsRestoring(false); + } + }; + + const copyKey = async () => { + try { + await navigator.clipboard.writeText(backupKey); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch (error) { + console.error(error); + setError('Failed to copy key to clipboard'); + } + }; + + return ( + + + Backup Controls + + + {error && ( + + {error} + + )} + +
+ + + {backupKey && ( +
+
+ Your backup key: + + {backupKey} + +
+ +
+ )} +
+ +
+ setKey(e.target.value)} + disabled={isRestoring} + /> + +
+
+
+ ); +} diff --git a/components/Header.tsx b/components/Header.tsx new file mode 100644 index 0000000..c5ee08f --- /dev/null +++ b/components/Header.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { ThemeToggle } from '@components/ThemeToggle'; +import { WavesIcon } from 'lucide-react'; + +export default function Header() { + return ( +
+
+
+
+
+
+
+
+ +
+
+ +
+
+
+ +

+ Blue Ocean Strategy +

+
+

+ Navigate to new market spaces and create uncontested growth +

+
+
+
+ ); +} diff --git a/components/ThemeProvider.tsx b/components/ThemeProvider.tsx new file mode 100644 index 0000000..b1a7360 --- /dev/null +++ b/components/ThemeProvider.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { ThemeProvider as NextThemesProvider } from 'next-themes'; + +export function ThemeProvider({ + children, + ...props +}: { + children: React.ReactNode; + [key: string]: unknown; +}) { + return {children}; +} diff --git a/components/ThemeToggle.tsx b/components/ThemeToggle.tsx new file mode 100644 index 0000000..9a85445 --- /dev/null +++ b/components/ThemeToggle.tsx @@ -0,0 +1,37 @@ +'use client'; + +import React from 'react'; +import { Moon, Sun } from 'lucide-react'; +import { useTheme } from 'next-themes'; +import { Button } from '@components/ui/button'; + +export function ThemeToggle() { + const { setTheme, resolvedTheme } = useTheme(); + + const [mounted, setMounted] = React.useState(false); + + React.useEffect(() => { + setMounted(true); + }, []); + + if (!mounted) { + return ( + + ); + } + + return ( + + ); +} diff --git a/components/core/FourActions.tsx b/components/core/FourActions.tsx new file mode 100644 index 0000000..32f7d90 --- /dev/null +++ b/components/core/FourActions.tsx @@ -0,0 +1,124 @@ +import React, { useState } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@components/ui/card'; +import { Button } from '@components/ui/button'; +import { Input } from '@components/ui/input'; +import { useGlobalState } from '@contexts/state/StateContext'; +import { Trash2, Plus } from 'lucide-react'; + +const FourActions = () => { + const { state, dispatch } = useGlobalState(); + const [newItems, setNewItems] = useState({ + eliminate: '', + reduce: '', + raise: '', + create: '', + }); + + const addItem = (actionType: keyof typeof newItems) => { + if (!newItems[actionType]) return; + + dispatch({ + type: 'ADD_ACTION', + payload: { + actionType, + value: newItems[actionType], + }, + }); + setNewItems((prev) => ({ ...prev, [actionType]: '' })); + }; + + const removeItem = ( + actionType: keyof typeof state.fourActions, + index: number + ) => { + dispatch({ + type: 'SET_STATE', + payload: { + ...state, + fourActions: { + ...state.fourActions, + [actionType]: state.fourActions[actionType].filter( + (_, i) => i !== index + ), + }, + }, + }); + }; + + const renderActionSection = ( + title: string, + actionType: keyof typeof state.fourActions, + description: string, + colorClasses: string + ) => ( + + + {title} + + +

{description}

+
+ + setNewItems((prev) => ({ ...prev, [actionType]: e.target.value })) + } + className="flex-1" + /> + +
+
+ {state.fourActions[actionType].map((item, index) => ( +
+ {item} + +
+ ))} +
+
+
+ ); + + return ( +
+ {renderActionSection( + 'Eliminate', + 'eliminate', + 'Which factors should be eliminated?', + 'border-red-500/50 dark:border-red-500/30 bg-red-50/50 dark:bg-red-950/10' + )} + {renderActionSection( + 'Reduce', + 'reduce', + 'Which factors should be reduced well below the industry standard?', + 'border-yellow-500/50 dark:border-yellow-500/30 bg-yellow-50/50 dark:bg-yellow-950/10' + )} + {renderActionSection( + 'Raise', + 'raise', + 'Which factors should be raised well above the industry standard?', + 'border-green-500/50 dark:border-green-500/30 bg-green-50/50 dark:bg-green-950/10' + )} + {renderActionSection( + 'Create', + 'create', + 'Which factors should be created that the industry has never offered?', + 'border-blue-500/50 dark:border-blue-500/30 bg-blue-50/50 dark:bg-blue-950/10' + )} +
+ ); +}; + +export default FourActions; diff --git a/components/core/PriceCorridor.tsx b/components/core/PriceCorridor.tsx new file mode 100644 index 0000000..57a0a37 --- /dev/null +++ b/components/core/PriceCorridor.tsx @@ -0,0 +1,301 @@ +import React, { useState } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@components/ui/card'; +import { Button } from '@components/ui/button'; +import { Input } from '@components/ui/input'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@components/ui/select'; +import { useGlobalState } from '@contexts/state/StateContext'; +import { Trash2, Plus } from 'lucide-react'; +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ReferenceLine, + ReferenceArea, + ResponsiveContainer, +} from 'recharts'; + +const PriceCorridor = () => { + const { state, dispatch } = useGlobalState(); + const [newCompetitor, setNewCompetitor] = useState<{ + name: string; + price: number; + category: 'same-form' | 'different-form' | 'different-function'; + }>({ + name: '', + price: 0, + category: 'same-form', + }); + + const setTargetPrice = (price: number) => { + dispatch({ + type: 'UPDATE_TARGET_PRICE', + payload: price, + }); + }; + + const addCompetitor = () => { + if (!newCompetitor.name || !newCompetitor.price) return; + dispatch({ + type: 'ADD_COMPETITOR', + payload: { + name: newCompetitor.name, + price: newCompetitor.price, + category: newCompetitor.category, + }, + }); + setNewCompetitor({ name: '', price: 0, category: 'same-form' }); + }; + + const removeCompetitor = (index: number) => { + dispatch({ + type: 'SET_STATE', + payload: { + ...state, + priceCorridor: { + ...state.priceCorridor, + competitors: state.priceCorridor.competitors.filter( + (_, i) => i !== index + ), + }, + }, + }); + }; + + const sortedCompetitors = [...state.priceCorridor.competitors].sort( + (a, b) => a.price - b.price + ); + + const prices = sortedCompetitors.map((c) => c.price); + const upperBound = Math.max(...prices, 0); + const lowerBound = Math.min(...prices, 0); + const range = upperBound - lowerBound; + + const corridorLower = lowerBound + range * 0.3; + const corridorUpper = lowerBound + range * 0.8; + + const chartData = sortedCompetitors.map((comp) => ({ + name: comp.name, + price: comp.price, + category: comp.category, + })); + + const categorizedCompetitors = { + 'same-form': sortedCompetitors.filter((c) => c.category === 'same-form'), + 'different-form': sortedCompetitors.filter( + (c) => c.category === 'different-form' + ), + 'different-function': sortedCompetitors.filter( + (c) => c.category === 'different-function' + ), + }; + + return ( +
+ + + Price Corridor of the Mass + + +
+ + + + + + + + {state.priceCorridor.targetPrice > 0 && ( + + )} + {chartData.length > 0 && ( + + )} + + +
+
+
+ +
+ + + Strategic Price + + +
+ {chartData.length > 0 && ( +
+

+ Suggested price corridor: {corridorLower.toFixed(2)} -{' '} + {corridorUpper.toFixed(2)} +

+

This range typically captures 70-80% of target buyers

+
+ )} +
+ setTargetPrice(Number(e.target.value))} + placeholder="Set target price..." + className="w-48" + /> + + Current target: {state.priceCorridor.targetPrice || 'Not set'} + +
+
+
+
+ + + + Add Alternative + + +
+ + setNewCompetitor((prev) => ({ + ...prev, + name: e.target.value, + })) + } + className="flex-1" + /> + + setNewCompetitor((prev) => ({ + ...prev, + price: Number(e.target.value), + })) + } + className="w-32" + /> + + +
+
+
+
+ + + + Price Alternatives Analysis + + +
+ {Object.entries(categorizedCompetitors).map( + ([category, competitors]) => ( +
+

+ {category.replace('-', ' ')} Alternatives +

+ {competitors.length > 0 ? ( +
+ {competitors.map((comp, index) => ( +
+ {comp.name} + {comp.price} + +
+ ))} +
+ ) : ( +

+ No alternatives added +

+ )} +
+ ) + )} +
+
+
+
+ ); +}; + +export default PriceCorridor; diff --git a/components/core/SixPaths.tsx b/components/core/SixPaths.tsx new file mode 100644 index 0000000..413cfa8 --- /dev/null +++ b/components/core/SixPaths.tsx @@ -0,0 +1,156 @@ +import React, { useState } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@components/ui/card'; +import { Button } from '@components/ui/button'; +import { Input } from '@components/ui/input'; +import { Textarea } from '@components/ui/textarea'; +import { useGlobalState } from '@contexts/state/StateContext'; +import { Trash2, Plus } from 'lucide-react'; +import type { PathType } from '@utils/types'; + +const pathDefinitions = { + industries: { + title: 'Alternative Industries', + description: 'Look across substitute and alternative industries', + }, + groups: { + title: 'Strategic Groups', + description: 'Look across strategic groups within your industry', + }, + buyers: { + title: 'Buyer Groups', + description: 'Look across chain of buyers', + }, + complementary: { + title: 'Complementary Products', + description: 'Look across complementary product and service offerings', + }, + functional: { + title: 'Functional/Emotional Appeal', + description: 'Look across functional or emotional appeal to buyers', + }, + trends: { + title: 'Time Trends', + description: 'Look across time and market trends', + }, +}; + +const SixPaths = () => { + const { state, dispatch } = useGlobalState(); + const [newOpportunities, setNewOpportunities] = useState< + Record + >({ + industries: '', + groups: '', + buyers: '', + complementary: '', + functional: '', + trends: '', + }); + + const addOpportunity = (pathType: PathType) => { + if (!newOpportunities[pathType]) return; + + dispatch({ + type: 'ADD_OPPORTUNITY', + payload: { + pathType, + value: newOpportunities[pathType], + }, + }); + setNewOpportunities((prev) => ({ ...prev, [pathType]: '' })); + }; + + const removeOpportunity = (pathType: PathType, index: number) => { + dispatch({ + type: 'SET_STATE', + payload: { + ...state, + sixPaths: { + ...state.sixPaths, + [pathType]: { + ...state.sixPaths[pathType], + opportunities: state.sixPaths[pathType].opportunities.filter( + (_, i) => i !== index + ), + }, + }, + }, + }); + }; + + const updateNotes = (pathType: PathType, notes: string) => { + dispatch({ + type: 'UPDATE_PATH_NOTES', + payload: { pathType, notes }, + }); + }; + + const renderPath = (pathType: PathType) => { + const { title, description } = pathDefinitions[pathType]; + + return ( + + + {title} + + +

{description}

+ +