This commit is contained in:
Flatlogic Bot 2026-02-28 13:06:56 +00:00
parent ad5e9eae31
commit 8c8258f7b8
17 changed files with 1390 additions and 159 deletions

View File

@ -0,0 +1,76 @@
module.exports = {
/**
* @param {import("sequelize").QueryInterface} queryInterface
* @param {import("sequelize").Sequelize} Sequelize
* @return {Promise<void>}
*/
async up(queryInterface, Sequelize) {
const createdAt = new Date();
const updatedAt = new Date();
await queryInterface.sequelize.query(`
INSERT INTO "rolesPermissionsPermissions" ("createdAt", "updatedAt", "roles_permissionsId", "permissionId")
SELECT
NOW(),
NOW(),
(SELECT id FROM "roles" WHERE name = 'Public'),
(SELECT id FROM "permissions" WHERE name = 'READ_STARTUPS')
WHERE EXISTS (SELECT id FROM "roles" WHERE name = 'Public')
AND EXISTS (SELECT id FROM "permissions" WHERE name = 'READ_STARTUPS')
ON CONFLICT DO NOTHING;
`);
await queryInterface.sequelize.query(`
INSERT INTO "rolesPermissionsPermissions" ("createdAt", "updatedAt", "roles_permissionsId", "permissionId")
SELECT
NOW(),
NOW(),
(SELECT id FROM "roles" WHERE name = 'Public'),
(SELECT id FROM "permissions" WHERE name = 'READ_STARTUP_CATEGORIES')
WHERE EXISTS (SELECT id FROM "roles" WHERE name = 'Public')
AND EXISTS (SELECT id FROM "permissions" WHERE name = 'READ_STARTUP_CATEGORIES')
ON CONFLICT DO NOTHING;
`);
await queryInterface.sequelize.query(`
INSERT INTO "rolesPermissionsPermissions" ("createdAt", "updatedAt", "roles_permissionsId", "permissionId")
SELECT
NOW(),
NOW(),
(SELECT id FROM "roles" WHERE name = 'Public'),
(SELECT id FROM "permissions" WHERE name = 'READ_STARTUP_TAGS')
WHERE EXISTS (SELECT id FROM "roles" WHERE name = 'Public')
AND EXISTS (SELECT id FROM "permissions" WHERE name = 'READ_STARTUP_TAGS')
ON CONFLICT DO NOTHING;
`);
await queryInterface.sequelize.query(`
INSERT INTO "rolesPermissionsPermissions" ("createdAt", "updatedAt", "roles_permissionsId", "permissionId")
SELECT
NOW(),
NOW(),
(SELECT id FROM "roles" WHERE name = 'Public'),
(SELECT id FROM "permissions" WHERE name = 'READ_STARTUP_TAG_ASSIGNMENTS')
WHERE EXISTS (SELECT id FROM "roles" WHERE name = 'Public')
AND EXISTS (SELECT id FROM "permissions" WHERE name = 'READ_STARTUP_TAG_ASSIGNMENTS')
ON CONFLICT DO NOTHING;
`);
},
/**
* @param {import("sequelize").QueryInterface} queryInterface
* @param {import("sequelize").Sequelize} Sequelize
* @return {Promise<void>}
*/
async down(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`
DELETE FROM "rolesPermissionsPermissions"
WHERE "roles_permissionsId" = (SELECT id FROM "roles" WHERE name = 'Public')
AND "permissionId" IN (
SELECT id FROM "permissions"
WHERE name IN ('READ_STARTUPS', 'READ_STARTUP_CATEGORIES', 'READ_STARTUP_TAGS', 'READ_STARTUP_TAG_ASSIGNMENTS')
);
`);
}
};

View File

@ -117,9 +117,9 @@ app.use('/api/roles', passport.authenticate('jwt', {session: false}), rolesRoute
app.use('/api/permissions', passport.authenticate('jwt', {session: false}), permissionsRoutes);
app.use('/api/startup_categories', passport.authenticate('jwt', {session: false}), startup_categoriesRoutes);
app.use('/api/startup_categories', startup_categoriesRoutes);
app.use('/api/startups', passport.authenticate('jwt', {session: false}), startupsRoutes);
app.use('/api/startups', startupsRoutes);
app.use('/api/startup_tags', passport.authenticate('jwt', {session: false}), startup_tagsRoutes);

View File

@ -0,0 +1,8 @@
export default {
i18n: {
defaultLocale: 'en',
locales: ['en', 'ru', 'fr', 'es', 'de'],
},
ns: ['common', 'translation'],
defaultNS: 'common',
};

View File

@ -2,12 +2,13 @@
* @type {import('next').NextConfig}
*/
const output = process.env.NODE_ENV === 'production' ? 'export' : 'standalone';
const nextConfig = {
trailingSlash: true,
const nextConfig = {
i18n: {
defaultLocale: 'en',
locales: ['en', 'ru', 'fr', 'es', 'de'],
},
trailingSlash: true,
distDir: 'build',
output,
basePath: "",
devIndicators: {
position: 'bottom-left',
},
@ -26,7 +27,15 @@ trailingSlash: true,
},
],
},
async rewrites() {
return [
{
source: '/startup/:id',
destination: '/web_pages/startup-details/?id=:id',
},
]
},
}
export default nextConfig
export default nextConfig

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1,72 @@
import React from 'react'
import { mdiRocketLaunch, mdiMapMarker, mdiTagOutline, mdiChartLine, mdiAccountGroupOutline } from '@mdi/js'
import BaseIcon from './BaseIcon'
interface Startup {
id: string
name: string
tagline: string
industry: string
location: string
stage: string
business_model: string
description?: string
logo?: { url: string }[]
}
const StartupSummaryTile = ({ startup }: { startup: Startup }) => {
const logoUrl = startup.logo && startup.logo[0] ? startup.logo[0].url : null
return (
<div className="bg-white p-6 rounded-3xl shadow-sm border border-slate-100 hover:shadow-xl hover:shadow-blue-100 transition-all duration-300">
<div className="flex items-center mb-6">
{logoUrl ? (
<img src={logoUrl} alt={startup.name} className="w-16 h-16 rounded-2xl object-cover border border-slate-100 mr-4" />
) : (
<div className="w-16 h-16 bg-blue-50 rounded-2xl flex items-center justify-center text-blue-600 border border-blue-100 mr-4">
<BaseIcon path={mdiRocketLaunch} size={32} />
</div>
)}
<div>
<h3 className="text-xl font-black text-slate-900 leading-tight">{startup.name}</h3>
<p className="text-sm font-medium text-slate-400 mt-1 uppercase tracking-wider">{startup.industry}</p>
</div>
</div>
<p className="text-slate-600 mb-6 font-medium line-clamp-2 italic leading-snug">
&quot;{startup.tagline}&quot;
</p>
<div className="space-y-4">
<div className="flex items-center text-slate-500 font-medium">
<div className="w-8 h-8 bg-slate-50 rounded-lg flex items-center justify-center mr-3 text-slate-400">
<BaseIcon path={mdiMapMarker} size={18} />
</div>
<span className="text-sm">{startup.location || 'Remote'}</span>
</div>
<div className="flex items-center text-slate-500 font-medium">
<div className="w-8 h-8 bg-slate-50 rounded-lg flex items-center justify-center mr-3 text-slate-400">
<BaseIcon path={mdiTagOutline} size={18} />
</div>
<span className="text-sm capitalize">{startup.stage} Stage</span>
</div>
<div className="flex items-center text-slate-500 font-medium">
<div className="w-8 h-8 bg-slate-50 rounded-lg flex items-center justify-center mr-3 text-slate-400">
<BaseIcon path={mdiChartLine} size={18} />
</div>
<span className="text-sm uppercase">{startup.business_model} Model</span>
</div>
</div>
<div className="mt-8 pt-6 border-t border-slate-50">
<button className="w-full bg-slate-900 hover:bg-slate-800 text-white py-3 rounded-2xl font-bold transition shadow-lg shadow-slate-200">
View Details
</button>
</div>
</div>
)
}
export default StartupSummaryTile

View File

@ -1,166 +1,86 @@
import React, { useEffect, useState } from 'react';
import React from 'react';
import type { ReactElement } from 'react';
import Head from 'next/head';
import Link from 'next/link';
import BaseButton from '../components/BaseButton';
import CardBox from '../components/CardBox';
import SectionFullScreen from '../components/SectionFullScreen';
import LayoutGuest from '../layouts/Guest';
import BaseDivider from '../components/BaseDivider';
import BaseButtons from '../components/BaseButtons';
import { getPageTitle } from '../config';
import { useAppSelector } from '../stores/hooks';
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
import {
mdiRocketLaunch,
mdiBriefcaseAccountOutline,
mdiArrowRight
} from '@mdi/js';
import BaseIcon from '../components/BaseIcon';
export default function LandingPage() {
return (
<div className="bg-slate-50 min-h-screen font-sans text-slate-900">
<Head>
<title>{getPageTitle('Welcome')}</title>
<meta name="description" content="Startup & Investor Marketplace" />
</Head>
export default function Starter() {
const [illustrationImage, setIllustrationImage] = useState({
src: undefined,
photographer: undefined,
photographer_url: undefined,
})
const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
const [contentType, setContentType] = useState('video');
const [contentPosition, setContentPosition] = useState('left');
const textColor = useAppSelector((state) => state.style.linkColor);
{/* Hero Section */}
<section className="pt-20 pb-32">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<div className="flex justify-center mb-6">
<div className="bg-blue-600 p-3 rounded-2xl shadow-lg shadow-blue-200">
<BaseIcon path={mdiRocketLaunch} className="text-white" size={40} />
</div>
</div>
<h1 className="text-5xl md:text-6xl font-black tracking-tight mb-6 text-slate-900">
The Future of <span className="text-blue-600">Investment</span>
</h1>
<p className="mt-4 max-w-2xl text-xl text-slate-500 mx-auto mb-16">
Connecting visionary startups with strategic investors through AI-driven insights and a seamless marketplace.
</p>
const title = 'Startup Profiles Marketplace'
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-4xl mx-auto">
{/* Startup Path */}
<Link href="/startup/submit-info" className="group relative bg-white p-10 rounded-3xl shadow-sm border border-slate-200 hover:border-blue-500 hover:shadow-xl hover:shadow-blue-100 transition-all duration-300 text-left">
<div className="w-16 h-16 bg-blue-50 rounded-2xl flex items-center justify-center mb-6 text-blue-600 group-hover:bg-blue-600 group-hover:text-white transition-colors">
<BaseIcon path={mdiRocketLaunch} size={32} />
</div>
<h3 className="text-2xl font-bold text-slate-900 mb-3 flex items-center">
I am a Startup
<BaseIcon path={mdiArrowRight} className="ml-2 opacity-0 group-hover:opacity-100 transform translate-x-[-10px] group-hover:translate-x-0 transition-all" size={24} />
</h3>
<p className="text-slate-500 text-lg leading-relaxed">
Showcase your startup, upload key documents, and get AI-powered insights to attract the right investors.
</p>
</Link>
// Fetch Pexels image/video
useEffect(() => {
async function fetchData() {
const image = await getPexelsImage();
const video = await getPexelsVideo();
setIllustrationImage(image);
setIllustrationVideo(video);
}
fetchData();
}, []);
{/* Investor Path */}
<Link href="/investor" className="group relative bg-white p-10 rounded-3xl shadow-sm border border-slate-200 hover:border-emerald-500 hover:shadow-xl hover:shadow-emerald-100 transition-all duration-300 text-left">
<div className="w-16 h-16 bg-emerald-50 rounded-2xl flex items-center justify-center mb-6 text-emerald-600 group-hover:bg-emerald-600 group-hover:text-white transition-colors">
<BaseIcon path={mdiBriefcaseAccountOutline} size={32} />
</div>
<h3 className="text-2xl font-bold text-slate-900 mb-3 flex items-center">
I am an Investor
<BaseIcon path={mdiArrowRight} className="ml-2 opacity-0 group-hover:opacity-100 transform translate-x-[-10px] group-hover:translate-x-0 transition-all" size={24} />
</h3>
<p className="text-slate-500 text-lg leading-relaxed">
Discover curated investment opportunities tailored to your risk profile and ethical preferences.
</p>
</Link>
</div>
</div>
</section>
const imageBlock = (image) => (
<div
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
style={{
backgroundImage: `${
image
? `url(${image?.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}}
>
<div className='flex justify-center w-full bg-blue-300/20'>
<a
className='text-[8px]'
href={image?.photographer_url}
target='_blank'
rel='noreferrer'
>
Photo by {image?.photographer} on Pexels
</a>
</div>
{/* Footer */}
<footer className="py-12 border-t border-slate-200 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex flex-col md:flex-row justify-between items-center">
<div className="flex items-center mb-4 md:mb-0">
<span className="font-bold text-xl text-slate-900 tracking-tight">StartupMarketplace</span>
</div>
<div className="text-slate-400 text-sm">
&copy; {new Date().getFullYear()} StartupMarketplace. All rights reserved.
</div>
</div>
</footer>
</div>
);
const videoBlock = (video) => {
if (video?.video_files?.length > 0) {
return (
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
<video
className='absolute top-0 left-0 w-full h-full object-cover'
autoPlay
loop
muted
>
<source src={video?.video_files[0]?.link} type='video/mp4'/>
Your browser does not support the video tag.
</video>
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
<a
className='text-[8px]'
href={video?.user?.url}
target='_blank'
rel='noreferrer'
>
Video by {video.user.name} on Pexels
</a>
</div>
</div>)
}
};
return (
<div
style={
contentPosition === 'background'
? {
backgroundImage: `${
illustrationImage
? `url(${illustrationImage.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}
: {}
}
>
<Head>
<title>{getPageTitle('Starter Page')}</title>
</Head>
<SectionFullScreen bg='violet'>
<div
className={`flex ${
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
} min-h-screen w-full`}
>
{contentType === 'image' && contentPosition !== 'background'
? imageBlock(illustrationImage)
: null}
{contentType === 'video' && contentPosition !== 'background'
? videoBlock(illustrationVideo)
: null}
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
<CardBoxComponentTitle title="Welcome to your Startup Profiles Marketplace app!"/>
<div className="space-y-3">
<p className='text-center text-gray-500'>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
<p className='text-center text-gray-500'>For guides and documentation please check
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
</div>
<BaseButtons>
<BaseButton
href='/login'
label='Login'
color='info'
className='w-full'
/>
</BaseButtons>
</CardBox>
</div>
</div>
</SectionFullScreen>
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
Privacy Policy
</Link>
</div>
</div>
);
}
Starter.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
LandingPage.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};

View File

@ -0,0 +1,75 @@
import React from 'react';
import type { ReactElement } from 'react';
import Head from 'next/head';
import Link from 'next/link';
import LayoutGuest from '../../layouts/Guest';
import { getPageTitle } from '../../config';
import {
mdiBriefcaseAccountOutline,
mdiChartPie,
mdiTableLarge,
mdiArrowRight
} from '@mdi/js';
import BaseIcon from '../../components/BaseIcon';
export default function InvestorChoicePage() {
return (
<div className="bg-slate-50 min-h-screen font-sans text-slate-900">
<Head>
<title>{getPageTitle('Investor Strategy')}</title>
<meta name="description" content="Choose your investment strategy" />
</Head>
{/* Strategy Choice Section */}
<section className="pt-20 pb-32">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<div className="flex justify-center mb-6">
<div className="bg-emerald-600 p-3 rounded-2xl shadow-lg shadow-emerald-200">
<BaseIcon path={mdiBriefcaseAccountOutline} className="text-white" size={40} />
</div>
</div>
<h1 className="text-5xl md:text-6xl font-black tracking-tight mb-6 text-slate-900">
Choose Your <span className="text-emerald-600">Strategy</span>
</h1>
<p className="mt-4 max-w-2xl text-xl text-slate-500 mx-auto mb-16">
Whether you want AI-curated opportunities or prefer to explore the marketplace, we&apos;ve got you covered.
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-4xl mx-auto">
{/* Tailored Flow */}
<Link href="/investor/tailored" className="group relative bg-white p-10 rounded-3xl shadow-sm border border-slate-200 hover:border-emerald-500 hover:shadow-xl hover:shadow-emerald-100 transition-all duration-300 text-left">
<div className="w-16 h-16 bg-emerald-50 rounded-2xl flex items-center justify-center mb-6 text-emerald-600 group-hover:bg-emerald-600 group-hover:text-white transition-colors">
<BaseIcon path={mdiChartPie} size={32} />
</div>
<h3 className="text-2xl font-bold text-slate-900 mb-3 flex items-center">
Tailored Strategy
<BaseIcon path={mdiArrowRight} className="ml-2 opacity-0 group-hover:opacity-100 transform translate-x-[-10px] group-hover:translate-x-0 transition-all" size={24} />
</h3>
<p className="text-slate-500 text-lg leading-relaxed">
Answer a few simple questions about your risk profile and preferences to receive a personalized portfolio visualization.
</p>
</Link>
{/* Manual Flow */}
<Link href="/investor/marketplace" className="group relative bg-white p-10 rounded-3xl shadow-sm border border-slate-200 hover:border-blue-500 hover:shadow-xl hover:shadow-blue-100 transition-all duration-300 text-left">
<div className="w-16 h-16 bg-blue-50 rounded-2xl flex items-center justify-center mb-6 text-blue-600 group-hover:bg-blue-600 group-hover:text-white transition-colors">
<BaseIcon path={mdiTableLarge} size={32} />
</div>
<h3 className="text-2xl font-bold text-slate-900 mb-3 flex items-center">
Manual Exploration
<BaseIcon path={mdiArrowRight} className="ml-2 opacity-0 group-hover:opacity-100 transform translate-x-[-10px] group-hover:translate-x-0 transition-all" size={24} />
</h3>
<p className="text-slate-500 text-lg leading-relaxed">
Browse our full marketplace of startups, apply advanced filters, and search for specific opportunities.
</p>
</Link>
</div>
</div>
</section>
</div>
);
}
InvestorChoicePage.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};

View File

@ -0,0 +1,161 @@
import React, { ReactElement, useEffect, useState } from 'react'
import Head from 'next/head'
import LayoutGuest from '../../layouts/Guest'
import SectionMain from '../../components/SectionMain'
import { getPageTitle } from '../../config'
import { useAppDispatch, useAppSelector } from '../../stores/hooks'
import { fetch as fetchStartups } from '../../stores/startups/startupsSlice'
import StartupSummaryTile from '../../components/StartupSummaryTile'
import { mdiMagnify, mdiFilterVariant, mdiClose } from '@mdi/js'
import BaseIcon from '../../components/BaseIcon'
import { Formik, Form, Field } from 'formik'
export default function MarketplacePage() {
const dispatch = useAppDispatch()
const { startups, loading } = useAppSelector((state) => state.startups)
const [search, setSearch] = useState('')
const [filters, setFilters] = useState({
industry: '',
stage: '',
business_model: '',
})
useEffect(() => {
const queryParts = []
if (search) queryParts.push(`name_contains=${search}`)
if (filters.industry) queryParts.push(`industry_contains=${filters.industry}`)
if (filters.stage) queryParts.push(`stage=${filters.stage}`)
if (filters.business_model) queryParts.push(`business_model=${filters.business_model}`)
const query = queryParts.length > 0 ? `?${queryParts.join('&')}` : ''
dispatch(fetchStartups({ query }))
}, [search, filters, dispatch])
return (
<div className="bg-slate-50 min-h-screen font-sans text-slate-900">
<Head>
<title>{getPageTitle('Startup Marketplace')}</title>
</Head>
<SectionMain>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex flex-col md:flex-row md:items-center justify-between mb-12 space-y-4 md:space-y-0">
<div>
<h1 className="text-4xl font-black text-slate-900">Startup Marketplace</h1>
<p className="text-slate-500 text-lg mt-2">Discover the next big opportunity.</p>
</div>
<div className="relative max-w-md w-full">
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
<BaseIcon path={mdiMagnify} className="text-slate-400" size={24} />
</div>
<input
type="text"
className="block w-full pl-12 pr-4 py-4 border-none rounded-2xl bg-white shadow-xl shadow-blue-50 focus:ring-2 focus:ring-blue-500 transition-all text-lg font-medium placeholder-slate-400"
placeholder="Search startups..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
</div>
<div className="flex flex-col md:flex-row gap-8">
{/* Sidebar Filters */}
<aside className="w-full md:w-64 flex-shrink-0">
<div className="bg-white p-6 rounded-3xl shadow-sm border border-slate-200 sticky top-24">
<div className="flex items-center mb-6 text-slate-900">
<BaseIcon path={mdiFilterVariant} className="mr-2" size={24} />
<h2 className="font-bold text-xl uppercase tracking-wider">Filters</h2>
</div>
<div className="space-y-6">
<div>
<label className="block text-sm font-bold text-slate-500 uppercase tracking-widest mb-3">Industry</label>
<select
className="w-full bg-slate-50 border-none rounded-xl py-3 px-4 font-medium focus:ring-2 focus:ring-blue-500 transition-all"
value={filters.industry}
onChange={(e) => setFilters({ ...filters, industry: e.target.value })}
>
<option value="">All Industries</option>
<option value="fintech">Fintech</option>
<option value="healthtech">Healthtech</option>
<option value="saas">SaaS</option>
<option value="edtech">Edtech</option>
<option value="ecommerce">E-commerce</option>
</select>
</div>
<div>
<label className="block text-sm font-bold text-slate-500 uppercase tracking-widest mb-3">Stage</label>
<select
className="w-full bg-slate-50 border-none rounded-xl py-3 px-4 font-medium focus:ring-2 focus:ring-blue-500 transition-all"
value={filters.stage}
onChange={(e) => setFilters({ ...filters, stage: e.target.value })}
>
<option value="">All Stages</option>
<option value="idea">Idea</option>
<option value="mvp">MVP</option>
<option value="beta">Beta</option>
<option value="launched">Launched</option>
<option value="growth">Growth</option>
</select>
</div>
<div>
<label className="block text-sm font-bold text-slate-500 uppercase tracking-widest mb-3">Business Model</label>
<select
className="w-full bg-slate-50 border-none rounded-xl py-3 px-4 font-medium focus:ring-2 focus:ring-blue-500 transition-all"
value={filters.business_model}
onChange={(e) => setFilters({ ...filters, business_model: e.target.value })}
>
<option value="">All Models</option>
<option value="saas">SaaS</option>
<option value="marketplace">Marketplace</option>
<option value="b2b">B2B</option>
<option value="b2c">B2C</option>
</select>
</div>
<button
onClick={() => setFilters({ industry: '', stage: '', business_model: '' })}
className="w-full flex items-center justify-center py-3 text-slate-400 hover:text-red-500 font-bold transition-colors"
>
<BaseIcon path={mdiClose} className="mr-1" size={18} />
Clear Filters
</button>
</div>
</div>
</aside>
{/* Main Content */}
<main className="flex-1">
{loading ? (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
</div>
) : startups && startups.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{startups.map((startup) => (
<StartupSummaryTile key={startup.id} startup={startup} />
))}
</div>
) : (
<div className="bg-white p-20 rounded-3xl shadow-sm border border-slate-100 text-center">
<div className="bg-slate-50 w-24 h-24 rounded-full flex items-center justify-center mx-auto mb-6 text-slate-300">
<BaseIcon path={mdiMagnify} size={48} />
</div>
<h3 className="text-2xl font-bold text-slate-900 mb-2">No startups found</h3>
<p className="text-slate-500 max-w-xs mx-auto">Try adjusting your filters or search terms to find what you&apos;re looking for.</p>
</div>
)}
</main>
</div>
</div>
</SectionMain>
</div>
)
}
MarketplacePage.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>
}

View File

@ -0,0 +1,209 @@
import React, { ReactElement, useState, useEffect } from 'react'
import Head from 'next/head'
import LayoutGuest from '../../layouts/Guest'
import SectionMain from '../../components/SectionMain'
import { getPageTitle } from '../../config'
import CardBox from '../../components/CardBox'
import BaseButton from '../../components/BaseButton'
import { mdiChartPie, mdiArrowRight, mdiChevronLeft, mdiHelpCircleOutline } from '@mdi/js'
import BaseIcon from '../../components/BaseIcon'
import dynamic from 'next/dynamic'
import { useAppDispatch, useAppSelector } from '../../stores/hooks'
import { fetch as fetchStartups } from '../../stores/startups/startupsSlice'
import StartupSummaryTile from '../../components/StartupSummaryTile'
// ApexCharts needs to be dynamic for Next.js SSR
const Chart = dynamic(() => import('react-apexcharts'), { ssr: false })
const questions = [
{
id: 'risk',
question: 'What is your risk tolerance?',
options: [
{ value: 'low', label: 'Conservative', description: 'Focus on stable, late-stage startups.' },
{ value: 'medium', label: 'Balanced', description: 'Mix of growth and early-stage companies.' },
{ value: 'high', label: 'Aggressive', description: 'High-reward early-stage opportunities.' },
]
},
{
id: 'sector',
question: 'Which sectors are you most interested in?',
options: [
{ value: 'fintech', label: 'Fintech', description: 'Financial technology and services.' },
{ value: 'healthtech', label: 'Healthtech', description: 'Healthcare and biotech innovations.' },
{ value: 'saas', label: 'SaaS', description: 'Software as a Service solutions.' },
{ value: 'sustainability', label: 'Sustainability', description: 'Green energy and eco-friendly tech.' },
]
},
{
id: 'ethics',
question: 'How important are ethical/ESG factors?',
options: [
{ value: 'essential', label: 'Essential', description: 'Only show companies with high ESG scores.' },
{ value: 'important', label: 'Important', description: 'Prefer companies with ethical practices.' },
{ value: 'neutral', label: 'Neutral', description: 'Ethics are not the primary driver.' },
]
}
]
export default function TailoredStrategyPage() {
const [step, setStep] = useState(0)
const [answers, setAnswers] = useState<Record<string, string>>({})
const [showResult, setShowResult] = useState(false)
const dispatch = useAppDispatch()
const { startups } = useAppSelector((state) => state.startups)
const [hoveredStartup, setHoveredStartup] = useState<any>(null)
useEffect(() => {
if (showResult) {
dispatch(fetchStartups({ query: '' }))
}
}, [showResult, dispatch])
const handleAnswer = (questionId: string, value: string) => {
setAnswers({ ...answers, [questionId]: value })
if (step < questions.length - 1) {
setStep(step + 1)
} else {
setShowResult(true)
}
}
// Mock data for the pie chart based on startups
const chartOptions: any = {
chart: {
type: 'pie',
events: {
dataPointMouseEnter: (event, chartContext, config) => {
const index = config.dataPointIndex
if (startups && startups[index]) {
setHoveredStartup(startups[index])
}
}
}
},
labels: startups?.slice(0, 5).map(s => s.name) || [],
legend: {
position: 'bottom'
},
colors: ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6'],
dataLabels: {
enabled: true
},
tooltip: {
enabled: true,
y: {
formatter: (val) => `${val}% Allocation`
}
}
}
const chartSeries = [30, 25, 20, 15, 10]
return (
<div className="bg-slate-50 min-h-screen font-sans text-slate-900">
<Head>
<title>{getPageTitle('Tailored Strategy')}</title>
</Head>
<SectionMain>
<div className="max-w-4xl mx-auto">
{!showResult ? (
<div className="space-y-12">
<div className="text-center">
<div className="inline-flex items-center justify-center p-3 bg-emerald-100 text-emerald-600 rounded-2xl mb-6">
<BaseIcon path={mdiHelpCircleOutline} size={32} />
</div>
<h1 className="text-4xl font-black text-slate-900 mb-4">Personalize Your Portfolio</h1>
<p className="text-slate-500 text-lg">Question {step + 1} of {questions.length}</p>
</div>
<div className="grid grid-cols-1 gap-6">
<h2 className="text-2xl font-bold text-center text-slate-800 mb-4">
{questions[step].question}
</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{questions[step].options.map((option) => (
<button
key={option.value}
onClick={() => handleAnswer(questions[step].id, option.value)}
className="bg-white p-8 rounded-3xl border-2 border-transparent hover:border-emerald-500 hover:shadow-xl transition-all duration-300 text-left group"
>
<div className="w-12 h-12 bg-emerald-50 rounded-xl flex items-center justify-center mb-6 text-emerald-600 group-hover:bg-emerald-600 group-hover:text-white transition-colors">
<BaseIcon path={mdiArrowRight} size={24} />
</div>
<h3 className="text-xl font-bold text-slate-900 mb-2">{option.label}</h3>
<p className="text-slate-500">{option.description}</p>
</button>
))}
</div>
</div>
{step > 0 && (
<div className="flex justify-center">
<BaseButton
label="Back"
icon={mdiChevronLeft}
onClick={() => setStep(step - 1)}
className="text-slate-400 hover:text-slate-600 font-bold"
/>
</div>
)}
</div>
) : (
<div className="space-y-12">
<div className="text-center">
<div className="inline-flex items-center justify-center p-3 bg-blue-100 text-blue-600 rounded-2xl mb-6">
<BaseIcon path={mdiChartPie} size={32} />
</div>
<h1 className="text-4xl font-black text-slate-900 mb-4">Your Tailored Portfolio</h1>
<p className="text-slate-500 text-lg">Based on your preferences, we recommend the following allocation.</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-12 items-center">
<div className="bg-white p-8 rounded-3xl shadow-xl border border-slate-100">
<Chart
options={chartOptions}
series={chartSeries}
type="pie"
width="100%"
/>
<p className="text-center text-sm text-slate-400 mt-6 italic">
Hover over segments to see individual startup details
</p>
</div>
<div className="space-y-6">
<h3 className="text-2xl font-bold text-slate-900">Featured Opportunities</h3>
{hoveredStartup ? (
<div className="transform scale-105 transition-all duration-500">
<StartupSummaryTile startup={hoveredStartup} />
</div>
) : (
<div className="bg-slate-100 border-2 border-dashed border-slate-200 rounded-3xl p-12 text-center text-slate-400">
<BaseIcon path={mdiArrowRight} size={48} className="mx-auto mb-4 opacity-20" />
<p className="font-medium text-lg">Hover over the chart to explore startups</p>
</div>
)}
</div>
</div>
<div className="flex justify-center pt-8">
<BaseButton
label="View Full Marketplace"
color="info"
className="px-12 py-4 rounded-2xl font-bold text-xl shadow-xl hover:shadow-blue-200 transition-all"
onClick={() => router.push('/investor/marketplace')}
/>
</div>
</div>
)}
</div>
</SectionMain>
</div>
)
}
TailoredStrategyPage.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>
}

View File

@ -0,0 +1,156 @@
import { mdiRocketLaunch, mdiCheckDecagram, mdiCreation } from '@mdi/js'
import Head from 'next/head'
import React, { ReactElement, useEffect, useState } from 'react'
import CardBox from '../../components/CardBox'
import LayoutGuest from '../../layouts/Guest'
import SectionMain from '../../components/SectionMain'
import { getPageTitle } from '../../config'
import BaseButton from '../../components/BaseButton'
import { fetch } from '../../stores/startups/startupsSlice'
import { aiResponse } from '../../stores/openAiSlice'
import { useAppDispatch, useAppSelector } from '../../stores/hooks'
import { useRouter } from 'next/router'
import BaseIcon from '../../components/BaseIcon'
import StartupSummaryTile from '../../components/StartupSummaryTile'
const AIInsightsPage = () => {
const router = useRouter()
const { id } = router.query
const dispatch = useAppDispatch()
const [startup, setStartup] = useState<any>(null)
const { isAskingResponse, aiResponse: rawAiResponse } = useAppSelector((state) => state.openAi)
const [insights, setInsights] = useState<string>('')
useEffect(() => {
if (id) {
dispatch(fetch({ id })).then((action) => {
if (fetch.fulfilled.match(action)) {
setStartup(action.payload)
// Trigger AI Insights
const prompt = `Analyze this startup and provide 3 key insights for investors,
focusing on market potential, competitive advantage, and growth strategy.
Startup Name: ${action.payload.name}
Tagline: ${action.payload.tagline}
Industry: ${action.payload.industry}
Description: ${action.payload.description}
Stage: ${action.payload.stage}
Business Model: ${action.payload.business_model}
`
dispatch(aiResponse({
input: [
{ role: 'system', content: 'You are an expert venture capital analyst.' },
{ role: 'user', content: prompt }
]
}))
}
})
}
}, [id, dispatch])
useEffect(() => {
if (rawAiResponse && rawAiResponse.success) {
// Extract text from Responses API format
const output = rawAiResponse.data?.output;
if (Array.isArray(output)) {
const message = output.find(o => o.type === 'message');
if (message && Array.isArray(message.content)) {
const textObj = message.content.find(c => c.type === 'output_text');
if (textObj) {
setInsights(textObj.text);
}
}
}
}
}, [rawAiResponse])
return (
<>
<Head>
<title>{getPageTitle('Step 3: AI Insights')}</title>
</Head>
<SectionMain>
<div className="max-w-4xl mx-auto">
{/* Progress Bar */}
<div className="flex items-center justify-between mb-12">
<div className="flex flex-col items-center">
<div className="w-10 h-10 bg-green-500 text-white rounded-full flex items-center justify-center font-bold mb-2 shadow-lg shadow-green-100">1</div>
<span className="text-sm font-semibold text-green-500">Info</span>
</div>
<div className="flex-1 h-1 bg-green-500 mx-4 -mt-6"></div>
<div className="flex flex-col items-center">
<div className="w-10 h-10 bg-green-500 text-white rounded-full flex items-center justify-center font-bold mb-2 shadow-lg shadow-green-100">2</div>
<span className="text-sm font-semibold text-green-500">Documents</span>
</div>
<div className="flex-1 h-1 bg-blue-600 mx-4 -mt-6"></div>
<div className="flex flex-col items-center">
<div className="w-10 h-10 bg-blue-600 text-white rounded-full flex items-center justify-center font-bold mb-2 shadow-lg shadow-blue-200">3</div>
<span className="text-sm font-semibold text-blue-600">Insights</span>
</div>
</div>
<div className="text-center mb-10">
<h1 className="text-3xl font-black text-slate-900 mb-3 flex items-center justify-center">
<BaseIcon path={mdiCreation} className="text-blue-600 mr-2" size={32} />
AI-Powered Insights
</h1>
<p className="text-slate-500 text-lg">Our AI has analyzed your startup profile. Here&apos;s what we found.</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 mb-12">
<div className="md:col-span-1">
<h2 className="text-xl font-bold text-slate-900 mb-4">Startup Summary</h2>
{startup && <StartupSummaryTile startup={startup} />}
</div>
<div className="md:col-span-2">
<CardBox className="h-full shadow-2xl border-none bg-blue-600 text-white">
<div className="mb-6 flex items-center">
<BaseIcon path={mdiCheckDecagram} size={24} className="mr-2" />
<h2 className="text-xl font-bold">Investment Analysis</h2>
</div>
{isAskingResponse ? (
<div className="flex flex-col items-center justify-center h-64 space-y-4">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-white"></div>
<p className="text-blue-100 font-medium">Analyzing market data and profile...</p>
</div>
) : (
<div className="prose prose-invert max-w-none">
{insights ? (
<div className="whitespace-pre-line text-blue-50 leading-relaxed text-lg">
{insights}
</div>
) : (
<p>AI Insights will appear here shortly.</p>
)}
</div>
)}
</CardBox>
</div>
</div>
<div className="flex justify-center space-x-6">
<BaseButton
label="Finish & Go to Marketplace"
color="info"
className="px-12 py-4 rounded-2xl font-bold text-xl shadow-xl hover:shadow-blue-200 transition-all"
onClick={() => router.push('/investor/marketplace')}
/>
</div>
</div>
</SectionMain>
</>
)
}
AIInsightsPage.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutGuest>
{page}
</LayoutGuest>
)
}
export default AIInsightsPage

View File

@ -0,0 +1,153 @@
import { mdiRocketLaunch, mdiArrowRight, mdiUpload } from '@mdi/js'
import Head from 'next/head'
import React, { ReactElement, useEffect, useState } from 'react'
import CardBox from '../../components/CardBox'
import LayoutGuest from '../../layouts/Guest'
import SectionMain from '../../components/SectionMain'
import { getPageTitle } from '../../config'
import { Field, Form, Formik } from 'formik'
import FormField from '../../components/FormField'
import BaseButton from '../../components/BaseButton'
import { update, fetch } from '../../stores/startups/startupsSlice'
import { useAppDispatch, useAppSelector } from '../../stores/hooks'
import { useRouter } from 'next/router'
import FormFilePicker from '../../components/FormFilePicker'
import FormImagePicker from '../../components/FormImagePicker'
const SubmitDocumentsPage = () => {
const router = useRouter()
const { id } = router.query
const dispatch = useAppDispatch()
const [initialValues, setInitialValues] = useState({
logo: [],
gallery_images: [],
attachments: [],
})
useEffect(() => {
if (id) {
dispatch(fetch({ id })).then((action) => {
if (fetch.fulfilled.match(action)) {
setInitialValues({
logo: action.payload.logo || [],
gallery_images: action.payload.gallery_images || [],
attachments: action.payload.attachments || [],
})
}
})
}
}, [id, dispatch])
const handleSubmit = async (data) => {
const resultAction = await dispatch(update({ id, data }))
if (update.fulfilled.match(resultAction)) {
await router.push(`/startup/ai-insights?id=${id}`)
}
}
return (
<>
<Head>
<title>{getPageTitle('Step 2: Submit Documents')}</title>
</Head>
<SectionMain>
<div className="max-w-3xl mx-auto">
{/* Progress Bar */}
<div className="flex items-center justify-between mb-12">
<div className="flex flex-col items-center">
<div className="w-10 h-10 bg-green-500 text-white rounded-full flex items-center justify-center font-bold mb-2 shadow-lg shadow-green-100">1</div>
<span className="text-sm font-semibold text-green-500">Info</span>
</div>
<div className="flex-1 h-1 bg-blue-600 mx-4 -mt-6"></div>
<div className="flex flex-col items-center">
<div className="w-10 h-10 bg-blue-600 text-white rounded-full flex items-center justify-center font-bold mb-2 shadow-lg shadow-blue-200">2</div>
<span className="text-sm font-semibold text-blue-600">Documents</span>
</div>
<div className="flex-1 h-1 bg-gray-200 mx-4 -mt-6"></div>
<div className="flex flex-col items-center">
<div className="w-10 h-10 bg-gray-200 text-gray-500 rounded-full flex items-center justify-center font-bold mb-2">3</div>
<span className="text-sm font-medium text-gray-400">Insights</span>
</div>
</div>
<div className="text-center mb-10">
<h1 className="text-3xl font-black text-slate-900 mb-3">Upload your documents</h1>
<p className="text-slate-500 text-lg">Bring your vision to life with visuals and deck.</p>
</div>
<CardBox className="shadow-2xl border-none">
<Formik
initialValues={initialValues}
enableReinitialize={true}
onSubmit={(values) => handleSubmit(values)}
>
<Form className="space-y-8">
<FormField label="Company Logo">
<Field
label="Logo"
color="info"
icon={mdiUpload}
path={'startups/logo'}
name="logo"
id="logo"
component={FormImagePicker}
/>
</FormField>
<FormField label="Pitch Deck and Other Documents">
<Field
label="Attachments"
color="info"
icon={mdiUpload}
path={'startups/attachments'}
name="attachments"
id="attachments"
component={FormFilePicker}
/>
</FormField>
<FormField label="Gallery Images">
<Field
label="Gallery"
color="info"
icon={mdiUpload}
path={'startups/gallery_images'}
name="gallery_images"
id="gallery_images"
component={FormImagePicker}
/>
</FormField>
<div className="flex justify-between pt-4">
<BaseButton
label="Back"
type="button"
onClick={() => router.push(`/startup/submit-info?id=${id}`)}
className="px-8 py-3 rounded-xl font-bold text-lg"
/>
<BaseButton
type="submit"
color="info"
label="Get AI Insights"
icon={mdiArrowRight}
className="px-8 py-3 rounded-xl font-bold text-lg"
/>
</div>
</Form>
</Formik>
</CardBox>
</div>
</SectionMain>
</>
)
}
SubmitDocumentsPage.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutGuest>
{page}
</LayoutGuest>
)
}
export default SubmitDocumentsPage

View File

@ -0,0 +1,139 @@
import { mdiRocketLaunch, mdiArrowRight } from '@mdi/js'
import Head from 'next/head'
import React, { ReactElement } from 'react'
import CardBox from '../../components/CardBox'
import LayoutGuest from '../../layouts/Guest'
import SectionMain from '../../components/SectionMain'
import { getPageTitle } from '../../config'
import { Field, Form, Formik } from 'formik'
import FormField from '../../components/FormField'
import BaseButton from '../../components/BaseButton'
import { create } from '../../stores/startups/startupsSlice'
import { useAppDispatch } from '../../stores/hooks'
import { useRouter } from 'next/router'
import BaseIcon from '../../components/BaseIcon'
const initialValues = {
name: '',
tagline: '',
description: '',
industry: '',
location: '',
stage: 'idea',
business_model: 'saas',
}
const SubmitInfoPage = () => {
const router = useRouter()
const dispatch = useAppDispatch()
const handleSubmit = async (data) => {
const resultAction = await dispatch(create(data))
if (create.fulfilled.match(resultAction)) {
const createdId = resultAction.payload.id
await router.push(`/startup/submit-documents?id=${createdId}`)
}
}
return (
<>
<Head>
<title>{getPageTitle('Step 1: Submit Info')}</title>
</Head>
<SectionMain>
<div className="max-w-3xl mx-auto">
{/* Progress Bar */}
<div className="flex items-center justify-between mb-12">
<div className="flex flex-col items-center">
<div className="w-10 h-10 bg-blue-600 text-white rounded-full flex items-center justify-center font-bold mb-2 shadow-lg shadow-blue-200">1</div>
<span className="text-sm font-semibold text-blue-600">Info</span>
</div>
<div className="flex-1 h-1 bg-gray-200 mx-4 -mt-6"></div>
<div className="flex flex-col items-center">
<div className="w-10 h-10 bg-gray-200 text-gray-500 rounded-full flex items-center justify-center font-bold mb-2">2</div>
<span className="text-sm font-medium text-gray-400">Documents</span>
</div>
<div className="flex-1 h-1 bg-gray-200 mx-4 -mt-6"></div>
<div className="flex flex-col items-center">
<div className="w-10 h-10 bg-gray-200 text-gray-500 rounded-full flex items-center justify-center font-bold mb-2">3</div>
<span className="text-sm font-medium text-gray-400">Insights</span>
</div>
</div>
<div className="text-center mb-10">
<h1 className="text-3xl font-black text-slate-900 mb-3">Tell us about your startup</h1>
<p className="text-slate-500 text-lg">Help us understand your vision and business model.</p>
</div>
<CardBox className="shadow-2xl border-none">
<Formik
initialValues={initialValues}
onSubmit={(values) => handleSubmit(values)}
>
<Form className="space-y-6">
<FormField label="Startup Name" help="What is the name of your company?">
<Field name="name" placeholder="e.g. Acme Corp" className="w-full" required />
</FormField>
<FormField label="Tagline" help="Summarize your startup in one sentence.">
<Field name="tagline" placeholder="e.g. The future of AI-driven logistics" className="w-full" required />
</FormField>
<FormField label="Industry" help="What sector are you operating in?">
<Field name="industry" placeholder="e.g. Fintech, Edtech, Healthtech" className="w-full" required />
</FormField>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<FormField label="Stage">
<Field name="stage" id="stage" component="select">
<option value="idea">Idea</option>
<option value="mvp">MVP</option>
<option value="beta">Beta</option>
<option value="launched">Launched</option>
<option value="growth">Growth</option>
<option value="scale">Scale</option>
</Field>
</FormField>
<FormField label="Business Model">
<Field name="business_model" id="business_model" component="select">
<option value="saas">SaaS</option>
<option value="marketplace">Marketplace</option>
<option value="b2b">B2B</option>
<option value="b2c">B2C</option>
<option value="other">Other</option>
</Field>
</FormField>
</div>
<FormField label="Description" help="A detailed description of your product or service.">
<Field name="description" as="textarea" placeholder="Describe your startup..." className="w-full h-32" required />
</FormField>
<div className="flex justify-end pt-4">
<BaseButton
type="submit"
color="info"
label="Next Step"
icon={mdiArrowRight}
className="px-8 py-3 rounded-xl font-bold text-lg"
/>
</div>
</Form>
</Formik>
</CardBox>
</div>
</SectionMain>
</>
)
}
SubmitInfoPage.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutGuest>
{page}
</LayoutGuest>
)
}
export default SubmitInfoPage

View File

@ -0,0 +1,249 @@
import React, { useEffect, useState } from 'react';
import type { ReactElement } from 'react';
import Head from 'next/head';
import { useRouter } from 'next/router';
import axios from 'axios';
import LayoutGuest from '../../layouts/Guest';
import SectionMain from '../../components/SectionMain';
import CardBox from '../../components/CardBox';
import UserAvatar from '../../components/UserAvatar';
import BaseIcon from '../../components/BaseIcon';
import {
mdiRocketLaunch,
mdiMapMarkerOutline,
mdiTagOutline,
mdiWeb,
mdiEmailOutline,
mdiGithub,
mdiTrendingUp,
mdiAccountGroup,
mdiCurrencyUsd,
mdiCalendarRange
} from '@mdi/js';
import BaseButton from '../../components/BaseButton';
import BaseButtons from '../../components/BaseButtons';
import { getPageTitle } from '../../config';
export default function StartupDetailsPage() {
const router = useRouter();
const { id } = router.query;
const [startup, setStartup] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
if (id && typeof id === 'string') {
const fetchStartup = async () => {
try {
setLoading(true);
const response = await axios.get(`/startups/${id}`);
setStartup(response.data);
} catch (err) {
console.error('Failed to fetch startup:', err);
setError(err);
} finally {
setLoading(false);
}
};
fetchStartup();
} else if (router.isReady && !id) {
setLoading(false);
}
}, [id, router.isReady]);
if (loading) {
return (
<SectionMain>
<div className="flex justify-center items-center min-h-64">
<p className="text-gray-500 text-lg">Loading startup profile...</p>
</div>
</SectionMain>
);
}
if (error || (!startup && !loading)) {
return (
<SectionMain>
<div className="text-center py-20 bg-white rounded-xl border">
<h2 className="text-2xl font-bold text-gray-800 mb-2">Startup Not Found</h2>
<p className="text-gray-500 mb-6">The startup profile you&apos;re looking for doesn&apos;t exist or is no longer available.</p>
<BaseButton label="Back to Marketplace" href="/" color="info" />
</div>
</SectionMain>
);
}
return (
<div className="bg-gray-50 min-h-screen pb-20">
<Head>
<title>{getPageTitle(startup.name || 'Startup Details')}</title>
</Head>
{/* Header / Hero */}
<div className="bg-white border-b py-12">
<div className="container mx-auto px-6">
<div className="flex flex-col md:flex-row items-center md:items-start text-center md:text-left">
<UserAvatar
username={startup.name}
className="w-32 h-32 md:mr-10 mb-6 md:mb-0"
/>
<div className="flex-grow">
<div className="flex flex-col md:flex-row md:items-center mb-2">
<h1 className="text-4xl font-bold text-gray-900 mr-4">{startup.name}</h1>
<span className="inline-block px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm font-semibold mt-2 md:mt-0 uppercase">
{startup.stage || 'IDEA'}
</span>
</div>
<p className="text-xl text-gray-600 mb-6 font-medium italic">
{startup.tagline || 'Innovation in the making.'}
</p>
<div className="flex flex-wrap justify-center md:justify-start gap-4 text-gray-500">
<div className="flex items-center">
<BaseIcon path={mdiMapMarkerOutline} className="mr-1" />
{startup.location || 'Remote'}
</div>
<div className="flex items-center">
<BaseIcon path={mdiTagOutline} className="mr-1" />
{startup.industry || 'Tech'}
</div>
<div className="flex items-center">
<BaseIcon path={mdiCalendarRange} className="mr-1" />
Founded {startup.founded_at ? new Date(startup.founded_at).getFullYear() : 'N/A'}
</div>
</div>
</div>
<div className="mt-8 md:mt-0 flex flex-col space-y-3 w-full md:w-auto">
<BaseButton label="Contact Founder" color="info" href="/login" className="w-full md:w-48" />
<BaseButton label="Watch Demo" outline color="info" href={startup.demo_url || '/login'} className="w-full md:w-48" />
</div>
</div>
</div>
</div>
<SectionMain>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Main Content */}
<div className="lg:col-span-2 space-y-8">
<CardBox>
<h2 className="text-2xl font-bold mb-4">About</h2>
<p className="text-gray-700 leading-relaxed whitespace-pre-wrap">
{startup.description || 'No detailed description available.'}
</p>
</CardBox>
<CardBox>
<h2 className="text-2xl font-bold mb-6">Business Metrics</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
<div className="flex items-start p-4 bg-gray-50 rounded-xl border border-gray-100">
<div className="p-3 bg-blue-100 rounded-lg mr-4">
<BaseIcon path={mdiTrendingUp} className="text-blue-600" />
</div>
<div>
<p className="text-sm text-gray-500 uppercase font-bold tracking-wider">Revenue</p>
<p className="text-xl font-bold">${parseFloat(startup.monthly_revenue || 0).toLocaleString()}/mo</p>
</div>
</div>
<div className="flex items-start p-4 bg-gray-50 rounded-xl border border-gray-100">
<div className="p-3 bg-green-100 rounded-lg mr-4">
<BaseIcon path={mdiAccountGroup} className="text-green-600" />
</div>
<div>
<p className="text-sm text-gray-500 uppercase font-bold tracking-wider">Team Size</p>
<p className="text-xl font-bold">{startup.team_size || '1-10'} members</p>
</div>
</div>
<div className="flex items-start p-4 bg-gray-50 rounded-xl border border-gray-100">
<div className="p-3 bg-purple-100 rounded-lg mr-4">
<BaseIcon path={mdiCurrencyUsd} className="text-purple-600" />
</div>
<div>
<p className="text-sm text-gray-500 uppercase font-bold tracking-wider">Funding Stage</p>
<p className="text-xl font-bold uppercase">{startup.funding_stage || 'Bootstrapped'}</p>
</div>
</div>
<div className="flex items-start p-4 bg-gray-50 rounded-xl border border-gray-100">
<div className="p-3 bg-orange-100 rounded-lg mr-4">
<BaseIcon path={mdiRocketLaunch} className="text-orange-600" />
</div>
<div>
<p className="text-sm text-gray-500 uppercase font-bold tracking-wider">Business Model</p>
<p className="text-xl font-bold uppercase">{startup.business_model || 'SaaS'}</p>
</div>
</div>
</div>
</CardBox>
</div>
{/* Sidebar */}
<div className="space-y-6">
<CardBox>
<h2 className="text-xl font-bold mb-4">Quick Links</h2>
<div className="space-y-4">
{startup.website_url && (
<a href={startup.website_url} target="_blank" rel="noreferrer" className="flex items-center p-3 hover:bg-gray-50 rounded-xl border transition group">
<div className="p-2 bg-gray-100 rounded-lg mr-3 group-hover:bg-blue-50 group-hover:text-blue-600 transition">
<BaseIcon path={mdiWeb} />
</div>
<span className="font-medium">Official Website</span>
</a>
)}
{startup.contact_email && (
<a href={`mailto:${startup.contact_email}`} className="flex items-center p-3 hover:bg-gray-50 rounded-xl border transition group">
<div className="p-2 bg-gray-100 rounded-lg mr-3 group-hover:bg-blue-50 group-hover:text-blue-600 transition">
<BaseIcon path={mdiEmailOutline} />
</div>
<span className="font-medium">Email Contact</span>
</a>
)}
{startup.github_url && (
<a href={startup.github_url} target="_blank" rel="noreferrer" className="flex items-center p-3 hover:bg-gray-50 rounded-xl border transition group">
<div className="p-2 bg-gray-100 rounded-lg mr-3 group-hover:bg-blue-50 group-hover:text-blue-600 transition">
<BaseIcon path={mdiGithub} />
</div>
<span className="font-medium">GitHub Repository</span>
</a>
)}
</div>
</CardBox>
<CardBox className="bg-blue-50 border-blue-100">
<h2 className="text-xl font-bold mb-4 text-blue-900">Seeking</h2>
<div className="space-y-3">
{startup.is_seeking_investment && (
<div className="flex items-center p-2 px-3 bg-white border border-blue-200 rounded-lg text-blue-700 font-bold">
<span className="mr-2">💰</span> Investment
</div>
)}
{startup.is_seeking_partners && (
<div className="flex items-center p-2 px-3 bg-white border border-blue-200 rounded-lg text-blue-700 font-bold">
<span className="mr-2">🤝</span> Partners
</div>
)}
{startup.is_hiring && (
<div className="flex items-center p-2 px-3 bg-white border border-blue-200 rounded-lg text-blue-700 font-bold">
<span className="mr-2">👨💻</span> Hiring
</div>
)}
{!startup.is_seeking_investment && !startup.is_seeking_partners && !startup.is_hiring && (
<p className="text-sm text-gray-500 italic">No specific needs listed at this time.</p>
)}
</div>
</CardBox>
<div className="p-6 bg-white rounded-xl border text-center">
<h3 className="font-bold mb-2">Interested in {startup.name}?</h3>
<p className="text-sm text-gray-500 mb-4">Create an account to save this startup to your favorites and get notified about updates.</p>
<BaseButton label="Join for Free" color="info" href="/login" className="w-full" />
</div>
</div>
</div>
</SectionMain>
</div>
);
}
StartupDetailsPage.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};

File diff suppressed because one or more lines are too long