feat: first draft

This commit is contained in:
2025-01-25 20:39:55 +01:00
parent 78c69dbc1a
commit 06fbbab24d
53 changed files with 13416 additions and 123 deletions

6
utils/cn.ts Normal file
View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

18
utils/redis.ts Normal file
View File

@@ -0,0 +1,18 @@
import { Redis } from '@upstash/redis';
if (!process.env.KV_REST_API_URL) {
throw new Error('KV_REST_API_URL is not defined');
}
if (!process.env.KV_REST_API_TOKEN) {
throw new Error('KV_REST_API_TOKEN is not defined');
}
export const redis = new Redis({
url: process.env.KV_REST_API_URL,
token: process.env.KV_REST_API_TOKEN,
retry: {
retries: 3,
backoff: (retryCount) => Math.min(retryCount * 100, 3000),
},
});

149
utils/stateReducer.ts Normal file
View File

@@ -0,0 +1,149 @@
import { GlobalState, PathType } from './types';
import { Action } from '@contexts/state/StateContext';
import { initialState } from './validateState';
export function stateReducer(state: GlobalState, action: Action): GlobalState {
switch (action.type) {
case 'SET_STATE':
return action.payload;
case 'RESET_STATE':
return initialState;
case 'ADD_FACTOR':
return {
...state,
strategyCanvas: {
...state.strategyCanvas,
factors: [...state.strategyCanvas.factors, action.payload],
},
};
case 'ADD_ACTION': {
const { actionType, value } = action.payload;
return {
...state,
fourActions: {
...state.fourActions,
[actionType]: [...state.fourActions[actionType], value],
},
};
}
case 'ADD_OPPORTUNITY': {
const { pathType, value: oppValue } = action.payload;
return {
...state,
sixPaths: {
...state.sixPaths,
[pathType]: {
...state.sixPaths[pathType],
opportunities: [
...state.sixPaths[pathType].opportunities,
oppValue,
],
},
},
};
}
case 'UPDATE_PATH_NOTES': {
const { pathType, notes } = action.payload as {
pathType: PathType;
notes: string;
};
return {
...state,
sixPaths: {
...state.sixPaths,
[pathType]: {
...state.sixPaths[pathType],
notes,
},
},
};
}
case 'TOGGLE_UTILITY': {
const { key } = action.payload;
const currentValue = state.utilityMap[key]?.value ?? false;
return {
...state,
utilityMap: {
...state.utilityMap,
[key]: {
...state.utilityMap[key],
value: !currentValue,
},
},
};
}
case 'UPDATE_UTILITY_NOTES':
return {
...state,
utilityMap: {
...state.utilityMap,
[action.payload.key]: {
...state.utilityMap[action.payload.key],
notes: action.payload.notes,
},
},
};
case 'UPDATE_TARGET_PRICE':
return {
...state,
priceCorridor: {
...state.priceCorridor,
targetPrice: action.payload,
},
};
case 'ADD_COMPETITOR':
return {
...state,
priceCorridor: {
...state.priceCorridor,
competitors: [...state.priceCorridor.competitors, action.payload],
},
};
case 'UPDATE_COMPETITOR': {
const { index, field, value: compValue } = action.payload;
return {
...state,
priceCorridor: {
...state.priceCorridor,
competitors: state.priceCorridor.competitors.map((comp, i) =>
i === index ? { ...comp, [field]: compValue } : comp
),
},
};
}
case 'TOGGLE_NON_CUSTOMER':
return {
...state,
validation: {
...state.validation,
nonCustomers: {
...state.validation.nonCustomers,
[action.payload.key]:
!state.validation.nonCustomers[action.payload.key],
},
},
};
case 'TOGGLE_SEQUENCE':
return {
...state,
validation: {
...state.validation,
sequence: {
...state.validation.sequence,
[action.payload.key]:
!state.validation.sequence[action.payload.key],
},
},
};
case 'UPDATE_VALIDATION_NOTES':
return {
...state,
validation: {
...state.validation,
notes: action.payload,
},
};
default:
return state;
}
}

52
utils/types.ts Normal file
View File

@@ -0,0 +1,52 @@
export interface GlobalState {
strategyCanvas: {
factors: {
id: string;
name: string;
marketScore: number;
ideaScore: number;
}[];
notes: Record<string, string>;
};
fourActions: {
eliminate: string[];
reduce: string[];
raise: string[];
create: string[];
};
sixPaths: Record<
PathType,
{
notes: string;
opportunities: string[];
}
>;
utilityMap: Record<
string,
{
value: boolean;
notes: string;
}
>;
priceCorridor: {
targetPrice: number;
competitors: {
name: string;
price: number;
category: 'same-form' | 'different-form' | 'different-function';
}[];
};
validation: {
nonCustomers: Record<string, boolean>;
sequence: Record<string, boolean>;
notes: string;
};
}
export type PathType =
| 'industries'
| 'groups'
| 'buyers'
| 'complementary'
| 'functional'
| 'trends';

80
utils/useStorage.ts Normal file
View File

@@ -0,0 +1,80 @@
import { GlobalState } from './types';
import { useCallback } from 'react';
import _ from 'lodash';
import { initialState, validateState } from './validateState';
export const useStorage = () => {
const saveState = useCallback(
_.debounce(async (state: GlobalState) => {
try {
const validState = validateState(state);
localStorage.setItem('state', JSON.stringify(validState));
} catch (error) {
console.error('Error saving state:', error);
throw error;
}
}, 1000),
[]
);
const loadState = useCallback(async (): Promise<GlobalState | null> => {
try {
const localState = localStorage.getItem('state');
return localState ? JSON.parse(localState) : null;
} catch (error) {
console.error('Error loading state:', error);
return null;
}
}, []);
const backupState = async (
state: GlobalState
): Promise<{ key?: string; error?: string }> => {
try {
const validState = validateState(state);
const res = await fetch('/api/backup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ state: validState }),
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.error || 'Backup request failed');
}
return { key: data.key };
} catch (error) {
console.error('Backup failed:', error);
return {
error:
error instanceof Error ? error.message : 'Unknown error occurred',
};
}
};
const restoreState = async (key: string): Promise<GlobalState> => {
try {
if (!key) {
throw new Error('No key provided');
}
const res = await fetch(`/api/restore?key=${encodeURIComponent(key)}`);
const data = await res.json();
if (!res.ok) {
throw new Error(
data.error || `Restore failed with status ${res.status}`
);
}
return validateState(data.data);
} catch (error) {
console.error('Restore failed:', error);
return initialState;
}
};
return { saveState, loadState, backupState, restoreState };
};

109
utils/validateState.ts Normal file
View File

@@ -0,0 +1,109 @@
import { GlobalState } from './types';
export const initialState: GlobalState = {
strategyCanvas: {
factors: [],
notes: {},
},
fourActions: {
eliminate: [],
reduce: [],
raise: [],
create: [],
},
sixPaths: {
industries: { notes: '', opportunities: [] },
groups: { notes: '', opportunities: [] },
buyers: { notes: '', opportunities: [] },
complementary: { notes: '', opportunities: [] },
functional: { notes: '', opportunities: [] },
trends: { notes: '', opportunities: [] },
},
utilityMap: {},
priceCorridor: {
targetPrice: 0,
competitors: [] as Array<{
name: string;
price: number;
category: 'same-form' | 'different-form' | 'different-function';
}>,
},
validation: {
nonCustomers: {},
sequence: {},
notes: '',
},
};
export function validateState(state: any): GlobalState {
if (!state || typeof state !== 'object') {
return initialState;
}
return {
strategyCanvas: {
factors: Array.isArray(state?.strategyCanvas?.factors)
? state.strategyCanvas.factors
: [],
notes: state?.strategyCanvas?.notes || {},
},
fourActions: {
eliminate: Array.isArray(state?.fourActions?.eliminate)
? state.fourActions.eliminate
: [],
reduce: Array.isArray(state?.fourActions?.reduce)
? state.fourActions.reduce
: [],
raise: Array.isArray(state?.fourActions?.raise)
? state.fourActions.raise
: [],
create: Array.isArray(state?.fourActions?.create)
? state.fourActions.create
: [],
},
sixPaths: {
industries: validatePathSection(state?.sixPaths?.industries),
groups: validatePathSection(state?.sixPaths?.groups),
buyers: validatePathSection(state?.sixPaths?.buyers),
complementary: validatePathSection(state?.sixPaths?.complementary),
functional: validatePathSection(state?.sixPaths?.functional),
trends: validatePathSection(state?.sixPaths?.trends),
},
utilityMap: state?.utilityMap || {},
priceCorridor: {
targetPrice: Number(state?.priceCorridor?.targetPrice) || 0,
competitors: Array.isArray(state?.priceCorridor?.competitors)
? state.priceCorridor.competitors.map(
(comp: { name?: string; price?: number; category?: string }) => ({
name: comp.name || '',
price: Number(comp.price) || 0,
category: [
'same-form',
'different-form',
'different-function',
].includes(comp.category || '')
? (comp.category as
| 'same-form'
| 'different-form'
| 'different-function')
: 'same-form',
})
)
: [],
},
validation: {
nonCustomers: state?.validation?.nonCustomers || {},
sequence: state?.validation?.sequence || {},
notes: state?.validation?.notes || '',
},
};
}
function validatePathSection(section: any) {
return {
notes: typeof section?.notes === 'string' ? section.notes : '',
opportunities: Array.isArray(section?.opportunities)
? section.opportunities
: [],
};
}