добавил презентацию

This commit is contained in:
Pavel Torbeev 2023-08-27 01:57:27 +03:00
parent 72505624d0
commit 903461baa7
45 changed files with 1235 additions and 80 deletions

View File

@ -14,6 +14,7 @@
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"axios": "^1.4.0",
"chart.js": "^4.4.0",
"clsx": "^2.0.0",
"date-fns": "^2.30.0",
"focus-visible": "^5.2.0",

49
src/api/deck/getDeck.ts Normal file
View File

@ -0,0 +1,49 @@
import {Question} from './types';
import {axios} from '../../lib/axios';
import {
DECK_API_URL,
QUESTION_API_URL,
QUESTION_PARAM_DECK_ID,
QUESTION_PARAM_QUESTION_ID,
} from './urlKeys';
import {ExtractFnReturnType, QueryConfig} from '../../lib/react-query';
import {useQuery} from '@tanstack/react-query';
import {QUERY_KEY_DECKS, QUERY_KEY_QUESTION} from './queryKeys';
export type GetDeckResponse = {
deck: {
name: string;
description: string;
},
slides: {
slide: number;
data: {
slug: string;
answer: any;
}[];
}[];
};
export const getDeck = ({ deckId }: { deckId: number }): Promise<GetDeckResponse> => {
return axios.get(
DECK_API_URL
.replace(`:${QUESTION_PARAM_DECK_ID}`, String(deckId))
);
};
type QueryFnType = typeof getDeck;
type UseDeckOptions = {
deckId: number;
config?: QueryConfig<QueryFnType>;
};
export const useDeck = ({ deckId, config }: UseDeckOptions) => {
return useQuery<ExtractFnReturnType<QueryFnType>>({
...config,
queryKey: [QUERY_KEY_DECKS, deckId],
queryFn: async () => {
return await getDeck({ deckId });
},
});
};

View File

@ -1,9 +1,11 @@
export const DECKS_API_URL = '/decks/';
export const FIRST_QUESTION_PARAM = 'deckId';
export const FIRST_QUESTION_API_URL = `/decks/question/:${FIRST_QUESTION_PARAM}`;
export const QUESTION_PARAM_DECK_ID = 'deckId';
export const QUESTION_PARAM_QUESTION_ID = 'questionId';
export const QUESTION_API_URL = `/decks/question/:${FIRST_QUESTION_PARAM}/:${QUESTION_PARAM_QUESTION_ID}`;
export const QUESTION_API_URL = `/decks/question/:${FIRST_QUESTION_PARAM}/:${QUESTION_PARAM_QUESTION_ID}`;
export const DECK_PARAM = 'deckId';
export const DECK_API_URL = `/decks/question/:${FIRST_QUESTION_PARAM}/presentation`;

View File

@ -1,6 +1,5 @@
$font-family-raleway: 'Raleway';
$font-family-ibm-plex-mono: 'IBM Plex Mono';
$font-family-lora: 'Lora', serif;
$font-family-default: system-ui, /* macOS 10.11-10.12 */ -apple-system, /* Windows 6+ */ 'Segoe UI',
/* Android 4+ */ 'Roboto', /* Ubuntu 10.10+ */ 'Ubuntu', /* Gnome 3+ */ 'Cantarell', /* KDE Plasma 5+ */ 'Noto Sans',
@ -152,10 +151,3 @@ $line-height-67: 67px;
font-weight: $font-weight-400;
line-height: $line-height-16;
}
@mixin text-presentation {
font-family: $font-family-lora;
font-size: $font-size-16;
font-weight: $font-weight-400;
line-height: $line-height-20;
}

View File

@ -45,12 +45,4 @@ $fonts-path: '../../assets/fonts' !default;
font-weight: $font-weight-400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: $font-family-lora;
src: url('#{$fonts-path}/Lora/Lora-Regular.ttf') format('truetype');
font-weight: $font-weight-400;
font-style: normal;
font-display: swap;
}

Binary file not shown.

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 3C11.4474 3 10.9994 3.448 10.9994 4.00065C10.9994 4.55329 11.4474 5.00129 12 5.00129C15.8653 5.00129 18.9987 8.13472 18.9987 12C18.9987 15.8653 15.8653 18.9987 12 18.9987C8.13472 18.9987 5.00129 15.8653 5.00129 12C5.00129 11.4474 4.55329 10.9994 4.00065 10.9994C3.448 10.9994 3 11.4474 3 12C3 16.9706 7.02944 21 12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 556 B

View File

@ -54,7 +54,7 @@
position: absolute;
top: -4px;
right: -4px;
background-color: white;
background-color: $color-on-surface-light-100;
z-index: 1;
padding: 2px 4px;
width: 16px;

View File

@ -0,0 +1,25 @@
@import 'src/app/styles/vars';
.Loader {
display: flex;
align-items: center;
width: 20px;
height: 20px;
}
.LoaderIcon {
width: 100%;
height: 100%;
color: $color-on-surface-dark-100;
animation: spinner 0.7s linear infinite;
}
@keyframes spinner {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@ -0,0 +1,35 @@
import { memo } from 'react';
import clsx from 'clsx';
import s from './Loader.module.scss';
import { ReactComponent as LoaderIcon } from '../../assets/icons/loader.svg';
// export enum LoaderSize {
// /**
// * Размер лоадера 20х20
// */
// small = 'small',
// /**
// * Размер лоадера 30х30
// */
// medium = 'medium',
// /**
// * Размер лоадера 40х40
// */
// large = 'large'
// }
export interface LoaderProps {
className?: string;
}
export const Loader = memo((props: LoaderProps) => {
const { className } = props;
return (
<div className={clsx(s.Loader, className)}>
<LoaderIcon className={clsx(s.LoaderIcon)} />
</div>
);
});
Loader.displayName = 'Loader';

View File

@ -0,0 +1 @@
export * from './Loader';

View File

@ -1,3 +1,3 @@
// export const API_URL = 'http://192.168.83.181:8000/api';
export const API_URL = 'http://192.168.83.181:8000/api';
// export const API_URL = 'http://192.168.22.4:8000/api';
export const API_URL = 'https://ed68-77-234-219-9.ngrok-free.app/api';
// export const API_URL = 'https://ed68-77-234-219-9.ngrok-free.app/api';

View File

@ -1,38 +1,28 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import reportWebVitals from './reportWebVitals';
// import {App} from './app';
import {App} from './app';
import {AppProvider} from './app';
import {PDFViewer, pdf} from '@react-pdf/renderer';
import {Document} from './pages/deck/document/Document';
// import 'focus-visible';
//
// import 'sanitize.css';
// import 'sanitize.css/forms.css';
// import 'sanitize.css/typography.css';
// import './app/styles/index.scss';
import 'focus-visible';
import 'sanitize.css';
import 'sanitize.css/forms.css';
import 'sanitize.css/typography.css';
import './app/styles/index.scss';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
// root.render(
// // <React.StrictMode>
// <AppProvider>
// <App />
// </AppProvider>
// // </React.StrictMode>
// );
root.render(
// <React.StrictMode>
<AppProvider>
<App />
</AppProvider>
// </React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
// reportWebVitals();
const App = () => (
<PDFViewer>
<Document />
</PDFViewer>
);
root.render(<App />);
reportWebVitals();

View File

@ -16,6 +16,8 @@ import {usePrevious} from '../../hooks/usePrevious';
import {generateAnswerFromData} from './utils/generateAnswerFromData';
import {generateTextFromAnswer} from './utils/generateTextFromAnswer';
import {Button, ButtonVariant} from '../../components/Button';
import {useNavigate} from 'react-router-dom';
import {DECK_PAGE_PARAM, DECK_PAGE_ROUTE} from '../../app/routes';
export interface ChatPageProps {
/**
@ -26,8 +28,8 @@ export interface ChatPageProps {
const QUESTION_POLLING_MS = 1000;
const DEFAULT_DECK_ID = 65;
const DEFAULT_QUESTION_ID = 32;
const DEFAULT_DECK_ID = 0;
const DEFAULT_QUESTION_ID = 0;
export const ChatPage: ReactFCC<ChatPageProps> = (props) => {
const {className} = props;
@ -115,6 +117,7 @@ export const ChatPage: ReactFCC<ChatPageProps> = (props) => {
const { mutateAsync: createAnswer } = useCreateAnswer();
const navigate = useNavigate();
const onSubmit: SubmitHandler<any> = useCallback(async (data) => {
if (!question || !data.value) {
@ -136,16 +139,24 @@ export const ChatPage: ReactFCC<ChatPageProps> = (props) => {
text: generateTextFromAnswer(question.type, answer)
});
setQuestionId(question!.next_id);
if (question.next_id) {
setQuestionId(question!.next_id);
} else {
navigate(DECK_PAGE_ROUTE.replace(`:${DECK_PAGE_PARAM}`, String(deckId)))
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [createAnswer, deckId, pushHistory, question, questionId]);
// Пропуск вопроса
const onSkip = useCallback(() => {
if (question && !question.params?.required) {
setQuestionId(question.next_id);
if (question.next_id) {
setQuestionId(question!.next_id);
} else {
navigate(DECK_PAGE_ROUTE.replace(`:${DECK_PAGE_PARAM}`, String(deckId)))
}
}
}, [question]);
}, [deckId, navigate, question]);
// ---------- Скролл чата ----------
// todo при печатании текста тоже двигать скролл

View File

@ -36,7 +36,6 @@ export const ChatFormMultipleDateDescription: ReactFCC<ChatFormMultipleDateDescr
for (const date in hint.value) {
newValue[format(new Date(date),'yyyy-MM-dd')] = hint.value[date]
}
console.log(hint.value);
setValue({ ...hint.value })
}}
>

View File

@ -49,7 +49,6 @@ export const ChatFormMultipleLinkDescription: ReactFCC<ChatFormMultipleLinkDescr
const newValue = { ...value };
const text = newValue[link];
delete newValue[link];
console.log()
onChange({ ...newValue, [e.target.value]: text })
}}
/>

View File

@ -0,0 +1,14 @@
@import 'src/app/styles/vars';
.DeckPage {
position: relative;
height: 100svh;
}
.DeckPage__loader {
width: 50px !important;
height: 50px !important;
position: absolute;
top: calc(50% - 25px);
left: calc(50% - 25px);
}

View File

@ -2,15 +2,89 @@ import {ReactFCC} from '../../utils/ReactFCC';
import {PDFViewer} from '@react-pdf/renderer';
import {useUrlParam} from '../../hooks/useUrlParam';
import {DECK_PAGE_PARAM} from '../../app/routes';
import {Document} from './document/Document';
import {MyDocument} from './document/Document';
import {useDeck} from '../../api/deck/getDeck';
import {useEffect, useRef, useState} from 'react';
import {generateMarketChart} from './document/media/generateMarketChart';
import {Loader} from '../../components/Loader';
import s from './DeckPage.module.scss';
import {generateFinancialChart} from './document/media/generateFinancialChart';
import {generateGrowChart} from './document/media/generateGrowChart';
export const DeckPage: ReactFCC = () => {
const deckId = useUrlParam(DECK_PAGE_PARAM, {parser: parseInt});
console.log(deckId);
const { data } = useDeck({
deckId: deckId ?? 0,
config: {
enabled: !!deckId
}
});
const [marketChart, setMarketChart] = useState('');
const [financialChart, setFinancialChart] = useState('');
const [growChart, setGrowChart] = useState('');
const [rendered, setRendered] = useState(false);
const initRef = useRef(false);
useEffect(() => {
if (!data || initRef.current) {
return;
}
initRef.current = true;
const market_values = data.slides[4].data.find((i) => i.slug === 'market_values')?.answer as any;
const users_metrics = data.slides[7].data.find((i) => i.slug === 'users_metrics')?.answer as any;
const company_value = data.slides[10].data.find((i) => i.slug === 'company_value')?.answer.sum as number;
const future_value = data.slides[10].data.find((i) => i.slug === 'future_value')?.answer.sum as number;
const time_to_spend = data.slides[10].data.find((i) => i.slug === 'time_to_spend')?.answer as any;
generateMarketChart(
'chart1',
[market_values.tam, market_values.sam, market_values.som],
(data) => setMarketChart(data)
);
generateFinancialChart(
'chart2',
[users_metrics.cac, users_metrics.ltv],
(data) => setFinancialChart(data)
);
generateGrowChart(
'chart3',
[company_value, future_value, time_to_spend],
(data) => setGrowChart(data)
);
}, [data]);
return (
<PDFViewer>
<Document />
</PDFViewer>
<div className={s.DeckPage}>
{!rendered && (
<Loader className={s.DeckPage__loader} />
)}
{data && marketChart && financialChart && growChart ? (
<PDFViewer style={{ width: '100vw', height: '100vh' }}>
<MyDocument
onRender={() => setRendered(true)}
data={data}
marketChart={marketChart}
financialChart={financialChart}
growChart={growChart}
/>
</PDFViewer>
) : (
<div style={{ visibility: 'hidden' }}>
<canvas id={'chart1'} />
<canvas id={'chart2'} />
<canvas id={'chart3'} />
</div>
)}
</div>
)
}

View File

@ -1,33 +1,54 @@
import {ReactFCC} from '../../../utils/ReactFCC';
import {Page, View, Text, StyleSheet} from '@react-pdf/renderer';
import {Document, Font} from '@react-pdf/renderer';
import {Slide1} from './slides/Slide1';
import {GetDeckResponse} from '../../../api/deck/getDeck';
import {Slide2} from './slides/Slide2';
import {Slide3} from './slides/Slide3';
import {Slide4} from './slides/Slide4';
import {Slide5} from './slides/Slide5';
import {Slide6} from './slides/Slide6';
import {Slide8} from './slides/Slide8';
import {Slide7} from './slides/Slide7';
import Chart from 'chart.js/auto';
import {Slide9} from './slides/Slide9';
import {Slide10} from './slides/Slide10';
import {Slide11} from './slides/Slide11';
import {Slide12} from './slides/Slide12';
import {Slide13} from './slides/Slide13';
export interface Props {
Chart.defaults.font.size = 20;
export interface MyDocumentProps {
data: GetDeckResponse;
onRender: () => void;
marketChart: string;
financialChart: string;
growChart: string;
}
const styles = StyleSheet.create({
page: {
flexDirection: 'row',
backgroundColor: '#E4E4E4'
},
section: {
margin: 10,
padding: 10,
flexGrow: 1
}
Font.register({
family: "Roboto",
src: "https://cdnjs.cloudflare.com/ajax/libs/ink/3.1.10/fonts/Roboto/roboto-medium-webfont.ttf"
});
export const Document: ReactFCC<Props> = (props) => {
export const MyDocument: ReactFCC<MyDocumentProps> = (props) => {
const { data, marketChart, financialChart, growChart, onRender } = props;
return (
<Document>
<Page>
<View>
<Text>Section #1</Text>
</View>
<View>
<Text>Section #2</Text>
</View>
</Page>
<Document onRender={onRender}>
<Slide1 title={data.deck.name} description={data.deck.description} data={data.slides[0].data} />
<Slide2 data={data.slides[1].data} />
<Slide3 data={data.slides[2].data} />
<Slide4 data={data.slides[3].data} />
<Slide5 data={data.slides[4].data} marketChart={marketChart} />
<Slide6 data={data.slides[5].data} />
<Slide7 data={data.slides[6].data} />
<Slide8 data={data.slides[7].data} financialChart={financialChart} />
<Slide9 data={data.slides[8].data} />
<Slide10 data={data.slides[9].data} />
<Slide11 data={data.slides[10].data} growChart={growChart} />
<Slide12 data={data.slides[11].data} />
<Slide13 data={data.slides[12].data} />
</Document>
);
};

View File

@ -0,0 +1,28 @@
import Chart from 'chart.js/auto';
export const generateFinancialChart = (rootId: string, [cac, ltv]: [number, number], callback: (data: string) => void) => {
const chart = new Chart((document.getElementById(rootId) as HTMLCanvasElement).getContext('2d') as CanvasRenderingContext2D, {
type: 'bar',
data: {
labels: ['LTV', 'CAC'],
datasets: [
{
label: 'Сравнение LTV (Lifetime Value) и CAC (Customer Acquisition Cost)',
data: [ltv, cac],
backgroundColor: 'rgba(255, 99, 132, 0.3)',
borderColor: 'rgba(255,99,132,1)',
borderWidth: 1,
},
],
},
options: {
indexAxis: 'y',
aspectRatio: 2,
animation: {
onComplete: function () {
callback(chart.toBase64Image());
},
},
},
})
}

View File

@ -0,0 +1,32 @@
import Chart from 'chart.js/auto';
import format from 'date-fns/format';
import addYears from 'date-fns/addYears';
export const generateGrowChart = (rootId: string, [company_value, future_value, date]: [number, number, Date], callback: (data: string) => void) => {
const chart = new Chart((document.getElementById(rootId) as HTMLCanvasElement).getContext('2d') as CanvasRenderingContext2D, {
type: 'line',
data: {
labels: [
format(new Date(), 'dd.MM.yyyy'),
format(date ? new Date(date) : addYears(new Date(), 1), 'dd.MM.yyyy')],
datasets: [
{
label: 'Рост после привлечения инвестиций',
data: [company_value, future_value],
fill: false,
borderColor: 'rgba(255, 99, 132, 0.5)',
tension: 0.1
},
],
},
options: {
// indexAxis: 'y',
aspectRatio: 2,
animation: {
onComplete: function () {
callback(chart.toBase64Image());
},
},
},
})
}

View File

@ -0,0 +1,28 @@
import Chart from 'chart.js/auto';
export const generateMarketChart = (rootId: string, [tam, sam, som]: [number, number, number], callback: (data: string) => void) => {
const chart = new Chart((document.getElementById(rootId) as HTMLCanvasElement).getContext('2d') as CanvasRenderingContext2D, {
type: 'bar',
data: {
labels: ['SAM', 'SOM'],
datasets: [
{
label: 'Сравнение SAM и SOM',
data: [sam, som],
backgroundColor: 'rgba(255, 99, 132, 0.3)',
borderColor: 'rgba(255,99,132,1)',
borderWidth: 1,
},
],
},
options: {
indexAxis: 'y',
aspectRatio: 2,
animation: {
onComplete: function () {
callback(chart.toBase64Image());
},
},
},
})
}

View File

@ -0,0 +1,2 @@
export const bgColor = '#EFEEE7';
export const primaryColor = '#2B2C30';

View File

@ -0,0 +1,81 @@
import {ReactFCC} from '../../../../utils/ReactFCC';
import {Page, StyleSheet, Text, View} from '@react-pdf/renderer';
import {bgColor, primaryColor} from '../shared';
import format from 'date-fns/format';
import {GetDeckResponse} from '../../../../api/deck/getDeck';
import {ExtractArray} from '../../../../utils/types';
export interface Slide1Props {
title: string;
description: string;
data: ExtractArray<GetDeckResponse['slides']>['data'];
}
const styles = StyleSheet.create({
page: {
display: 'flex',
flexDirection: 'column',
alignItems: 'stretch',
backgroundColor: bgColor,
color: primaryColor,
padding: '56px',
fontFamily: 'Roboto'
},
mainBlock: {
flex: '1',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'stretch'
},
title: {
fontSize: 96,
width: '100%',
},
divider: {
width: '100%',
height: 1,
backgroundColor: primaryColor,
margin: '0 0'
},
description: {
marginTop: '16px',
width: '100%',
fontSize: '20px',
},
secondaryBlock: {
//
},
info: {
display: 'flex',
flexDirection: 'column',
rowGap: 4
},
infoText: {
fontFamily: 'Helvetica',
fontSize: '14px',
}
});
export const Slide1: ReactFCC<Slide1Props> = (props) => {
const { title, description, data } = props;
const name = data.find((i) => i.slug === 'names')?.answer as string;
return (
<Page size="A4" orientation={'landscape'} style={styles.page}>
<View style={styles.mainBlock}>
<Text style={styles.title}>{name}</Text>
<View style={styles.divider} />
<Text style={styles.description}>{description}</Text>
</View>
<View style={styles.secondaryBlock}>
<View style={styles.info}>
<Text style={styles.infoText}>Henrietta Mitchell, Founder & CEO</Text>
<Text style={styles.infoText}>{format(new Date(), 'dd MMM, yyyy')}</Text>
</View>
</View>
</Page>
)
}

View File

@ -0,0 +1,69 @@
import {ReactFCC} from '../../../../utils/ReactFCC';
import {Image, Page, StyleSheet, Text, View} from '@react-pdf/renderer';
import {bgColor, primaryColor} from '../shared';
import {GetDeckResponse} from '../../../../api/deck/getDeck';
import {ExtractArray} from '../../../../utils/types';
import {currencyFormatter, formatDate} from '../../../../utils/fomat';
export interface Slide10Props {
data: ExtractArray<GetDeckResponse['slides']>['data'];
}
const styles = StyleSheet.create({
page: {
display: 'flex',
flexDirection: 'column',
alignItems: 'stretch',
backgroundColor: bgColor,
color: primaryColor,
fontFamily: 'Roboto',
padding: '48px',
},
title: {
fontSize: 36,
width: '100%',
letterSpacing: 3,
textTransform: 'uppercase'
},
divider: {
width: '100%',
height: 1,
backgroundColor: primaryColor,
margin: '8px 0 16px'
},
text: {
width: '100%',
fontSize: '20px',
marginBottom: 24
},
subtitle: {
width: '100%',
fontSize: '24px',
},
});
export const Slide10: ReactFCC<Slide10Props> = (props) => {
const { data } = props;
const how_much_investments = data.find((i) => i.slug === 'how_much_investments')?.answer.sum as any;
const time_to_spend = data.find((i) => i.slug === 'time_to_spend')?.answer as any;
const investments_sold = data.find((i) => i.slug === 'investments_sold')?.answer as any;
return (
<Page size="A4" orientation={'landscape'} style={styles.page}>
<View>
<Text style={styles.title}>Инвестиционный раунд</Text>
<View style={styles.divider} />
<Text style={styles.subtitle}>Сколько инвестиций необходимо</Text>
<Text style={styles.text}>{currencyFormatter.format(how_much_investments)}</Text>
<Text style={styles.subtitle}>Цели инвестиций</Text>
<Text style={styles.text}>{investments_sold}</Text>
<Text style={styles.subtitle}>Срок освоения инвестиций</Text>
<Text style={styles.text}>{formatDate(time_to_spend)}</Text>
</View>
</Page>
)
}

View File

@ -0,0 +1,63 @@
import {ReactFCC} from '../../../../utils/ReactFCC';
import {Image, Page, StyleSheet, Text, View} from '@react-pdf/renderer';
import {bgColor, primaryColor} from '../shared';
import {GetDeckResponse} from '../../../../api/deck/getDeck';
import {ExtractArray} from '../../../../utils/types';
export interface Slide11Props {
data: ExtractArray<GetDeckResponse['slides']>['data'];
growChart: string;
}
const styles = StyleSheet.create({
page: {
display: 'flex',
flexDirection: 'column',
alignItems: 'stretch',
backgroundColor: bgColor,
color: primaryColor,
fontFamily: 'Roboto',
padding: '48px',
},
title: {
fontSize: 36,
width: '100%',
letterSpacing: 3,
textTransform: 'uppercase'
},
divider: {
width: '100%',
height: 1,
backgroundColor: primaryColor,
margin: '8px 0 16px'
},
text: {
width: '100%',
fontSize: '20px',
marginBottom: 24
},
subtitle: {
width: '100%',
fontSize: '24px',
},
image: {
maxWidth: '600px',
maxHeight: '300px',
margin: '50 auto 0'
}
});
export const Slide11: ReactFCC<Slide11Props> = (props) => {
const { data, growChart } = props;
return (
<Page size="A4" orientation={'landscape'} style={styles.page}>
<View>
<Text style={styles.title}>Инвестиционный раунд</Text>
<View style={styles.divider} />
<Image style={styles.image} src={growChart} />
</View>
</Page>
)
}

View File

@ -0,0 +1,91 @@
import {ReactFCC} from '../../../../utils/ReactFCC';
import {Image, Page, StyleSheet, Text, View} from '@react-pdf/renderer';
import {bgColor, primaryColor} from '../shared';
import {GetDeckResponse} from '../../../../api/deck/getDeck';
import {ExtractArray} from '../../../../utils/types';
import {formatDate} from '../../../../utils/fomat';
import {Fragment} from 'react';
export interface Slide12Props {
data: ExtractArray<GetDeckResponse['slides']>['data'];
}
const styles = StyleSheet.create({
page: {
display: 'flex',
flexDirection: 'column',
alignItems: 'stretch',
backgroundColor: bgColor,
color: primaryColor,
fontFamily: 'Roboto',
padding: '48px',
},
title: {
fontSize: 36,
width: '100%',
letterSpacing: 3,
textTransform: 'uppercase'
},
divider: {
width: '100%',
height: 1,
backgroundColor: primaryColor,
margin: '8px 0 16px'
},
subtitle: {
fontSize: '18px',
marginBottom: '4px',
marginLeft: '-6px'
},
text: {
fontSize: '14px',
marginLeft: '-6px'
},
map: {
display: 'flex',
flexDirection: 'row',
marginTop: 70
},
item: {
flex: 1,
display: 'flex',
flexDirection: 'column',
borderTop: '1px solid black',
padding: '16px 16px 0 0',
},
dot: {
width: 8,
height: 8,
backgroundColor: 'black',
borderRadius: '50%',
marginTop: '-3.5px'
}
});
export const Slide12: ReactFCC<Slide12Props> = (props) => {
const { data } = props;
const aims = data.find((i) => i.slug === 'aims')?.answer as any;
return (
<Page size="A4" orientation={'landscape'} style={styles.page}>
<View>
<Text style={styles.title}>Роадмап</Text>
<View style={styles.divider} />
<View style={styles.map}>
<View style={styles.dot} />
{Object.entries(aims).map(([date, text], index) => (
<Fragment key={index}>
<View style={styles.item}>
<Text style={styles.subtitle}>{formatDate(date)}</Text>
<Text style={styles.text}>{text as string}</Text>
</View>
<View style={styles.dot} />
</Fragment>
))}
</View>
</View>
</Page>
)
}

View File

@ -0,0 +1,67 @@
import {ReactFCC} from '../../../../utils/ReactFCC';
import {Image, Page, StyleSheet, Text, View} from '@react-pdf/renderer';
import {bgColor, primaryColor} from '../shared';
import {GetDeckResponse} from '../../../../api/deck/getDeck';
import {ExtractArray} from '../../../../utils/types';
import {formatDate} from '../../../../utils/fomat';
import {Fragment} from 'react';
export interface Slide13Props {
data: ExtractArray<GetDeckResponse['slides']>['data'];
}
const styles = StyleSheet.create({
page: {
display: 'flex',
flexDirection: 'column',
alignItems: 'stretch',
backgroundColor: bgColor,
color: primaryColor,
fontFamily: 'Roboto',
padding: '48px',
},
title: {
fontSize: 36,
width: '100%',
letterSpacing: 3,
textTransform: 'uppercase'
},
divider: {
width: '100%',
height: 1,
backgroundColor: primaryColor,
margin: '8px 0 16px'
},
text: {
width: '100%',
fontSize: '20px',
marginBottom: 24
},
subtitle: {
width: '100%',
fontSize: '24px',
marginBottom: 24
},
});
export const Slide13: ReactFCC<Slide13Props> = (props) => {
const { data } = props;
const links = data.find((i) => i.slug === 'links')?.answer as any;
return (
<Page size="A4" orientation={'landscape'} style={styles.page}>
<View>
<Text style={styles.title}>Контакты</Text>
<View style={styles.divider} />
{Object.entries(links).map(([name, link], index) => (
<Fragment key={index}>
<Text style={styles.subtitle}>{`${name}: ${link}`}</Text>
{/*<Text style={styles.text}>{text as string}</Text>*/}
</Fragment>
))}
</View>
</Page>
)
}

View File

@ -0,0 +1,53 @@
import {ReactFCC} from '../../../../utils/ReactFCC';
import {Page, StyleSheet, Text, View} from '@react-pdf/renderer';
import {bgColor, primaryColor} from '../shared';
import {GetDeckResponse} from '../../../../api/deck/getDeck';
import {ExtractArray} from '../../../../utils/types';
export interface Slide2Props {
data: ExtractArray<GetDeckResponse['slides']>['data'];
}
const styles = StyleSheet.create({
page: {
display: 'flex',
flexDirection: 'column',
alignItems: 'stretch',
backgroundColor: bgColor,
color: primaryColor,
fontFamily: 'Roboto',
padding: '48px',
},
title: {
fontSize: 36,
width: '100%',
letterSpacing: 3,
textTransform: 'uppercase'
},
divider: {
width: '100%',
height: 1,
backgroundColor: primaryColor,
margin: '8px 0 16px'
},
text: {
width: '100%',
fontSize: '20px',
},
});
export const Slide2: ReactFCC<Slide2Props> = (props) => {
const { data } = props;
const problem = data.find((i) => i.slug === 'problems')?.answer as string;
return (
<Page size="A4" orientation={'landscape'} style={styles.page}>
<View>
<Text style={styles.title}>Проблема</Text>
<View style={styles.divider} />
<Text style={styles.text}>{problem}</Text>
</View>
</Page>
)
}

View File

@ -0,0 +1,53 @@
import {ReactFCC} from '../../../../utils/ReactFCC';
import {Page, StyleSheet, Text, View} from '@react-pdf/renderer';
import {bgColor, primaryColor} from '../shared';
import {GetDeckResponse} from '../../../../api/deck/getDeck';
import {ExtractArray} from '../../../../utils/types';
export interface SlideProps {
data: ExtractArray<GetDeckResponse['slides']>['data'];
}
const styles = StyleSheet.create({
page: {
display: 'flex',
flexDirection: 'column',
alignItems: 'stretch',
backgroundColor: bgColor,
color: primaryColor,
fontFamily: 'Roboto',
padding: '48px',
},
title: {
fontSize: 36,
width: '100%',
letterSpacing: 3,
textTransform: 'uppercase'
},
divider: {
width: '100%',
height: 1,
backgroundColor: primaryColor,
margin: '8px 0 16px'
},
text: {
width: '100%',
fontSize: '20px',
},
});
export const Slide3: ReactFCC<SlideProps> = (props) => {
const { data } = props;
const actuality = data.find((i) => i.slug === 'actuality')?.answer as string;
return (
<Page size="A4" orientation={'landscape'} style={styles.page}>
<View>
<Text style={styles.title}>Описание и ценностное предложение стартапа</Text>
<View style={styles.divider} />
<Text style={styles.text}>{actuality}</Text>
</View>
</Page>
)
}

View File

@ -0,0 +1,56 @@
import {ReactFCC} from '../../../../utils/ReactFCC';
import {Page, StyleSheet, Text, View} from '@react-pdf/renderer';
import {bgColor, primaryColor} from '../shared';
import {GetDeckResponse} from '../../../../api/deck/getDeck';
import {ExtractArray} from '../../../../utils/types';
export interface SlideProps {
data: ExtractArray<GetDeckResponse['slides']>['data'];
}
const styles = StyleSheet.create({
page: {
display: 'flex',
flexDirection: 'column',
alignItems: 'stretch',
backgroundColor: bgColor,
color: primaryColor,
fontFamily: 'Roboto',
padding: '48px',
},
title: {
fontSize: 36,
width: '100%',
letterSpacing: 3,
textTransform: 'uppercase'
},
divider: {
width: '100%',
height: 1,
backgroundColor: primaryColor,
margin: '8px 0 16px'
},
text: {
width: '100%',
fontSize: '20px',
marginBottom: 24
},
});
export const Slide4: ReactFCC<SlideProps> = (props) => {
const { data } = props;
const solve = data.find((i) => i.slug === 'solve')?.answer as string;
const works = data.find((i) => i.slug === 'works')?.answer as string;
return (
<Page size="A4" orientation={'landscape'} style={styles.page}>
<View>
<Text style={styles.title}>Решение</Text>
<View style={styles.divider} />
<Text style={styles.text}>{solve}</Text>
<Text style={styles.text}>{works}</Text>
</View>
</Page>
)
}

View File

@ -0,0 +1,62 @@
import {ReactFCC} from '../../../../utils/ReactFCC';
import {Image, Page, StyleSheet, Text, View} from '@react-pdf/renderer';
import {bgColor, primaryColor} from '../shared';
import {GetDeckResponse} from '../../../../api/deck/getDeck';
import {ExtractArray} from '../../../../utils/types';
export interface Slide5Props {
data: ExtractArray<GetDeckResponse['slides']>['data'];
marketChart: string;
}
const styles = StyleSheet.create({
page: {
display: 'flex',
flexDirection: 'column',
alignItems: 'stretch',
backgroundColor: bgColor,
color: primaryColor,
fontFamily: 'Roboto',
padding: '48px',
},
title: {
fontSize: 36,
width: '100%',
letterSpacing: 3,
textTransform: 'uppercase'
},
divider: {
width: '100%',
height: 1,
backgroundColor: primaryColor,
margin: '8px 0 16px'
},
text: {
width: '100%',
fontSize: '20px',
marginBottom: 24
},
image: {
maxWidth: '600px',
maxHeight: '300px',
margin: '0 auto'
}
});
export const Slide5: ReactFCC<Slide5Props> = (props) => {
const { data, marketChart } = props;
const market_values = data.find((i) => i.slug === 'market_values')?.answer as any;
const users = data.find((i) => i.slug === 'users')?.answer as string;
return (
<Page size="A4" orientation={'landscape'} style={styles.page}>
<View>
<Text style={styles.title}>Рынок</Text>
<View style={styles.divider} />
<Text style={styles.text}>Целевая аудитория: {users}</Text>
<Image style={styles.image} src={marketChart} />
</View>
</Page>
)
}

View File

@ -0,0 +1,71 @@
import {ReactFCC} from '../../../../utils/ReactFCC';
import {Image, Page, StyleSheet, Text, View} from '@react-pdf/renderer';
import {bgColor, primaryColor} from '../shared';
import {GetDeckResponse} from '../../../../api/deck/getDeck';
import {ExtractArray} from '../../../../utils/types';
export interface Slide6Props {
data: ExtractArray<GetDeckResponse['slides']>['data'];
}
const styles = StyleSheet.create({
page: {
display: 'flex',
flexDirection: 'column',
alignItems: 'stretch',
backgroundColor: bgColor,
color: primaryColor,
fontFamily: 'Roboto',
padding: '48px',
},
title: {
fontSize: 36,
width: '100%',
letterSpacing: 3,
textTransform: 'uppercase'
},
divider: {
width: '100%',
height: 1,
backgroundColor: primaryColor,
margin: '8px 0 16px'
},
text: {
width: '100%',
fontSize: '20px',
marginBottom: 24
},
subtitle: {
width: '100%',
fontSize: '24px',
},
});
export const Slide6: ReactFCC<Slide6Props> = (props) => {
const { data } = props;
const competitors = data.find((i) => i.slug === 'competitors')?.answer as string;
const competitors_strength = data.find((i) => i.slug === 'competitors_strength')?.answer as string;
const competitors_low = data.find((i) => i.slug === 'competitors_low')?.answer as string;
const advantages = data.find((i) => i.slug === 'advantages')?.answer as string;
return (
<Page size="A4" orientation={'landscape'} style={styles.page}>
<View>
<Text style={styles.title}>Конкуренты</Text>
<View style={styles.divider} />
<Text style={styles.text}>{competitors}</Text>
<Text style={styles.subtitle}>Сильные стороны</Text>
<Text style={styles.text}>{competitors_strength}</Text>
<Text style={styles.subtitle}>Слабые стороны</Text>
<Text style={styles.text}>{competitors_low}</Text>
<Text style={styles.subtitle}>Наши преимущества</Text>
<Text style={styles.text}>{advantages}</Text>
</View>
</Page>
)
}

View File

@ -0,0 +1,59 @@
import {ReactFCC} from '../../../../utils/ReactFCC';
import {Image, Page, StyleSheet, Text, View} from '@react-pdf/renderer';
import {bgColor, primaryColor} from '../shared';
import {GetDeckResponse} from '../../../../api/deck/getDeck';
import {ExtractArray} from '../../../../utils/types';
export interface Slide7Props {
data: ExtractArray<GetDeckResponse['slides']>['data'];
}
const styles = StyleSheet.create({
page: {
display: 'flex',
flexDirection: 'column',
alignItems: 'stretch',
backgroundColor: bgColor,
color: primaryColor,
fontFamily: 'Roboto',
padding: '48px',
},
title: {
fontSize: 36,
width: '100%',
letterSpacing: 3,
textTransform: 'uppercase'
},
divider: {
width: '100%',
height: 1,
backgroundColor: primaryColor,
margin: '8px 0 16px'
},
text: {
width: '100%',
fontSize: '20px',
marginBottom: 24
},
subtitle: {
width: '100%',
fontSize: '24px',
},
});
export const Slide7: ReactFCC<Slide7Props> = (props) => {
const { data } = props;
const money = data.find((i) => i.slug === 'money')?.answer as string;
return (
<Page size="A4" orientation={'landscape'} style={styles.page}>
<View>
<Text style={styles.title}>Бизнес-модель</Text>
<View style={styles.divider} />
<Text style={styles.text}>{money}</Text>
</View>
</Page>
)
}

View File

@ -0,0 +1,63 @@
import {ReactFCC} from '../../../../utils/ReactFCC';
import {Image, Page, StyleSheet, Text, View} from '@react-pdf/renderer';
import {bgColor, primaryColor} from '../shared';
import {GetDeckResponse} from '../../../../api/deck/getDeck';
import {ExtractArray} from '../../../../utils/types';
export interface Slide8Props {
data: ExtractArray<GetDeckResponse['slides']>['data'];
financialChart: string;
}
const styles = StyleSheet.create({
page: {
display: 'flex',
flexDirection: 'column',
alignItems: 'stretch',
backgroundColor: bgColor,
color: primaryColor,
fontFamily: 'Roboto',
padding: '48px',
},
title: {
fontSize: 36,
width: '100%',
letterSpacing: 3,
textTransform: 'uppercase'
},
divider: {
width: '100%',
height: 1,
backgroundColor: primaryColor,
margin: '8px 0 16px'
},
text: {
width: '100%',
fontSize: '20px',
marginBottom: 24
},
image: {
maxWidth: '600px',
maxHeight: '300px',
margin: '0 auto'
}
});
export const Slide8: ReactFCC<Slide8Props> = (props) => {
const { data, financialChart } = props;
const how_much_investments = data.find((i) => i.slug === 'how_much_investments')?.answer.sum as any;
const financial_indicators = data.find((i) => i.slug === 'financial_indicators')?.answer as any;
const users_metrics = data.find((i) => i.slug === 'users_metrics')?.answer as any;
return (
<Page size="A4" orientation={'landscape'} style={styles.page}>
<View>
<Text style={styles.title}>Финансы</Text>
<View style={styles.divider} />
<Text style={styles.text}>{financial_indicators}</Text>
<Image style={styles.image} src={financialChart} />
</View>
</Page>
)
}

View File

@ -0,0 +1,55 @@
import {ReactFCC} from '../../../../utils/ReactFCC';
import {Image, Page, StyleSheet, Text, View} from '@react-pdf/renderer';
import {bgColor, primaryColor} from '../shared';
import {GetDeckResponse} from '../../../../api/deck/getDeck';
import {ExtractArray} from '../../../../utils/types';
export interface Slide9Props {
data: ExtractArray<GetDeckResponse['slides']>['data'];
}
const styles = StyleSheet.create({
page: {
display: 'flex',
flexDirection: 'column',
alignItems: 'stretch',
backgroundColor: bgColor,
color: primaryColor,
fontFamily: 'Roboto',
padding: '48px',
},
title: {
fontSize: 36,
width: '100%',
letterSpacing: 3,
textTransform: 'uppercase'
},
divider: {
width: '100%',
height: 1,
backgroundColor: primaryColor,
margin: '8px 0 16px'
},
text: {
width: '100%',
fontSize: '20px',
marginBottom: 24
},
});
export const Slide9: ReactFCC<Slide9Props> = (props) => {
const { data } = props;
// const how_much_investments = data.find((i) => i.slug === 'how_much_investments')?.answer as any;
// const users_metrics = data.find((i) => i.slug === 'users_metrics')?.answer as any;
return (
<Page size="A4" orientation={'landscape'} style={styles.page}>
<View>
<Text style={styles.title}>Команда</Text>
{/*<View style={styles.divider} />*/}
{/*<Text style={styles.text}>{how_much_investments}</Text>*/}
</View>
</Page>
)
}

View File

@ -9,6 +9,8 @@ export type IntrinsicPropsWithoutRef<E extends keyof JSX.IntrinsicElements> = Re
JSX.IntrinsicElements[E]
>;
export type ExtractArray<T> = T extends (infer U)[] ? U : T;
export type AnchorPropsWithoutRef = IntrinsicPropsWithoutRef<'a'>;
export type DivPropsWithoutRef = IntrinsicPropsWithoutRef<'div'>;
export type PPropsWithoutRef = IntrinsicPropsWithoutRef<'p'>;

View File

@ -1606,6 +1606,11 @@
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
"@kurkle/color@^0.3.0":
version "0.3.2"
resolved "https://registry.yarnpkg.com/@kurkle/color/-/color-0.3.2.tgz#5acd38242e8bde4f9986e7913c8fdf49d3aa199f"
integrity sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==
"@leichtgewicht/ip-codec@^2.0.1":
version "2.0.4"
resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b"
@ -3380,6 +3385,13 @@ char-regex@^2.0.0:
resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-2.0.1.tgz#6dafdb25f9d3349914079f010ba8d0e6ff9cd01e"
integrity sha512-oSvEeo6ZUD7NepqAat3RqoucZ5SeqLJgOvVIwkafu6IP3V0pO38s/ypdVUmDDK6qIIHNlYHJAKX9E7R7HoKElw==
chart.js@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-4.4.0.tgz#df843fdd9ec6bd88d7f07e2b95348d221bd2698c"
integrity sha512-vQEj6d+z0dcsKLlQvbKIMYFHd3t8W/7L2vfJIbYcfyPcRx92CsHqECpueN8qVGNlKyDcr5wBrYAYKnfu/9Q1hQ==
dependencies:
"@kurkle/color" "^0.3.0"
check-types@^11.1.1:
version "11.2.2"
resolved "https://registry.yarnpkg.com/check-types/-/check-types-11.2.2.tgz#7afc0b6a860d686885062f2dba888ba5710335b4"