Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: full-startup-page #117

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ yarn-error.log*
.env*.local

.vercel
*.lock
26 changes: 26 additions & 0 deletions app/e-lab/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import Stat from "@components/Stat";
import Testimonials from "@components/Testimonials";
import Timeline from "@components/Timeline";
import Section from "@components/ui/Section";
import StartupList from "@components/ELabStartupList";
import {
faBook,
faCircleNodes,
faHandshakeSimple,
faHandsHoldingCircle,
} from "@fortawesome/free-solid-svg-icons";
import { faq, testimonials } from "data/e-lab";
import {startups} from "data/e-lab-startups";
import Link from "next/link";
import { Hero } from "./hero";
import type { Metadata } from "next";
Expand Down Expand Up @@ -254,6 +256,30 @@ export default function Page() {
/>
</Section>

<Section className="bg-purple-950 text-white">
<h2 className="mb-12 bg-gradient-to-r from-yellow-500 to-red-500 bg-clip-text text-center text-3xl font-semibold uppercase tracking-widest text-transparent sm:text-5xl">
E-Lab Startup Directory
</h2>
<p className="mb-8 text-center text-4xl">
Since <span className="text-yellow-500">2022</span>, we have founded{" "}
<span className="text-yellow-500">16 startups</span> within the{" "}
{" "}
<span className="text-yellow-500">E-LAB</span>
</p>
<div className="flex flex-col gap-8 lg:px-24 xl:px-44">
<p className="px-8 text-center">
We are proud to present the startups that have emerged from the AI
E-Lab program. Each of them has a unique story and a vision to
change the world with their AI-based solutions. We invite you to
explore the startups and their founders, get inspired by their
innovative ideas, and discover exciting job opportunities available
with these pioneering companies.
</p>
</div>
<ul style={{ marginTop: '20px' }}></ul>
<StartupList startups={startups} maxHeight="500px" />
</Section>

<Section className="bg-purple-950 text-white">
<h2 className="mb-12 bg-gradient-to-r from-yellow-500 to-red-500 bg-clip-text text-center text-3xl font-semibold uppercase tracking-widest text-transparent sm:text-5xl">
Meet our Partners and Sponsors
Expand Down
18 changes: 18 additions & 0 deletions app/e-lab/startups/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { startups, Startup} from '@data/e-lab-startups';
import StartupDetails from '@components/StartupDetails';

export default function StartupPage({ params }: { params: { id: string } }) {

const startup: Startup | undefined = startups.find((startup: Startup) => {
if (startup && startup.id === params.id) {
return startup;
}
return undefined;
});

if (!startup) {
return <div>Startup Not Found</div>;
}

return <StartupDetails startup={startup} />;
}
2 changes: 1 addition & 1 deletion app/hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const Hero = () => {
distort={0.3}
wireframe={true}
wireframeLinewidth={5}
color={fullConfig.theme.colors.purple["600"]}
color={(fullConfig.theme?.colors?.purple?.["600"] as string) ?? "#000000"}
transparent
opacity={0.4}
blending={THREE.AdditiveBlending}
Expand Down
181 changes: 181 additions & 0 deletions components/ELabStartupList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
"use client";

import React, { useState, useEffect, useRef } from 'react';
import { Startup } from '../data/e-lab-startups';
import Link from "next/link";
import '../styles/tags.css';
import Image from "next/image";

interface StartupListProps {
startups: Startup[];
maxHeight: string; // Adding height prop
}

type FilterOptions = Record<string, string[]>;

export default function StartupList({ startups, maxHeight }: StartupListProps) {
const [searchQuery, setSearchQuery] = useState('');
const [selectedFilters, setSelectedFilters] = useState<FilterOptions>({});
const [filteredStartups, setFilteredStartups] = useState<Startup[]>(startups);
const [isFilterVisible, setIsFilterVisible] = useState(false);

const sidebarRef = useRef<HTMLDivElement>(null)
const checkboxesRef = useRef<Record<string, HTMLInputElement>>({});

const filterCategories: (keyof Startup)[] = ['tag', 'batch', 'industry'];

// Filter startups
const filterOptions = startups.reduce((acc, startup) => {
filterCategories.forEach((category) => {
if (startup[category] && !acc[category]?.includes(startup[category] as string)) {
acc[category]?.push(startup[category] as string);
}
});
return acc;
}, filterCategories.reduce((acc, category) => ({ ...acc, [category]: [] as string[] }), {} as Record<string, string[]>));

useEffect(() => {
const applyFilters = () => {
const filtered = startups.filter((startup) => {
const matchesSearchQuery = searchQuery
? startup.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
startup.description.toLowerCase().includes(searchQuery.toLowerCase())
: true;

const matchesFilters = Object.entries(selectedFilters).every(([key, values]) => {
if (!values.length) return true;
return values.includes(startup[key as keyof Startup] as string);
});

return matchesFilters && matchesSearchQuery;
});
setFilteredStartups(filtered);
};

applyFilters();
}, [selectedFilters, searchQuery, startups]);

const handleFilterChange = (category: string, value: string, isChecked: boolean) => {
setSelectedFilters((prevFilters) => {
const updatedFilters = { ...prevFilters };

// Ensure the category array exists
if (!updatedFilters[category]) {
updatedFilters[category] = [];
}

if (isChecked) {
updatedFilters[category]!.push(value); // Use non-null assertion operator (!)
} else {
updatedFilters[category] = updatedFilters[category]!.filter((v) => v !== value); // Use non-null assertion operator (!)
}

return updatedFilters;
});
};

const resetFilters = () => {
setSelectedFilters({});
setSearchQuery('');

Object.values(checkboxesRef.current).forEach((checkbox) => {
if (checkbox) {
checkbox.checked = false;
}
});
};

// Filter visibility for mobile view
const toggleFilterVisibility = () => {
setIsFilterVisible(!isFilterVisible);
};

const handleClickOutside = (event: MouseEvent) => {
if (sidebarRef.current && !sidebarRef.current.contains(event.target as Node)) {
setIsFilterVisible(false);
}
};

useEffect(() => {
if (isFilterVisible) {
document.addEventListener('mousedown', handleClickOutside);
} else {
document.removeEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isFilterVisible]);


return (
<div className="flex flex-col md:flex-row">

{/* Filter*/}
<div className={`fixed inset-0 z-50 flex bg-opacity-75 ${isFilterVisible ? 'block' : 'hidden'} md:static md:block`}>
<div ref={sidebarRef} className="max-w-60 flex flex-col p-4 bg-purple-950 rounded shadow-lg md:static md:flex md:flex-col md:p-4 md:border md:border-white md:rounded">
<h4 className="text-lg font-semibold mb-4">Filters</h4>
{filterCategories.map((category) => (
<div key={category} className="mb-4">
<h5 className="font-medium mb-2">{category.charAt(0).toUpperCase() + category.slice(1)}</h5>
{filterOptions[category]?.map((option) => (
<div key={option} className="flex items-center mb-1">
<input
type="checkbox"
id={`${category}-${option}`}
name={`${category}-${option}`}
value={option}
ref={(el) => {
if (el) checkboxesRef.current[`${category}-${option}`] = el;
}}
onChange={(e) => handleFilterChange(category, option, e.target.checked)}
className="mr-2"
/>
<label htmlFor={`${category}-${option}`} className="text-sm overflow-hidden text-overflow-ellipsis">{option}</label>
</div>
))}
</div>
))}
<div className="flex justify-center" >
<button onClick={resetFilters} className="mt-4 px-4 py-2 bg-purple-500 text-white rounded">Reset Filters</button>
</div>
</div>
</div>

{/* Main Content */}
<main className="flex-1 p-4 pt-0" style={{ maxHeight: maxHeight }}>
{/* Filter Button for Mobile View */}
<div className="block w-full md:hidden mb-4">
<button onClick={toggleFilterVisibility} className="w-full p-3 bg-purple-500 rounded">Filter</button>
</div>
<div className="mb-4">
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search startups..."
className="w-full px-4 py-2 border rounded text-black"
/>
</div>
<div className="overflow-y-auto scrollbar-hidden scrollbar-purple pr-4" style={{ maxHeight: `calc(${maxHeight} - 90px)` }}>
{filteredStartups.map((startup) => (
<Link key={startup.id} href={`/e-lab/startups/${startup.id}`}>
<div className="mb-4 p-4 border rounded-lg shadow flex items-center flex-col md:flex-row">
<Image src={startup.logo} alt={`${startup.name} logo`} className="w-16 h-16 object-contain mr-4" width={0} height={0} sizes="100vw"/>
<div>
<h5 className="text-md font-medium">{startup.name}</h5>
<p className="text-sm text-gray-300">{startup.description}</p>
<div className="flex flex-wrap">
<span className="tag mr-2 mt-2 ">{startup.batch}</span>
<span className="tag mr-2 mt-2 ">{startup.industry}</span>
<span className="tag mr-2 mt-2 ">{startup.tag}</span>
</div>
</div>
</div>
</Link>
))}
</div>
</main>
</div>
);
}
64 changes: 64 additions & 0 deletions components/StartupDetails.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"use client";

import Image from 'next/image';
import Link from 'next/link';
import { Startup } from '@data/e-lab';
import { useState } from 'react';

const StartupDetails = ({ startup }: { startup: Startup }) => {
const [imageError, setImageError] = useState(false);

return (
<div className="bg-purple-950 text-white min-h-screen pt-16 p-8">
<div className="max-w-4xl mx-auto">

<div className="flex items-center justify-between mb-8" style={{ height: '100px', width: '100px' }}>
<h1 className="text-4xl font-bold">{startup.name}</h1>
{!imageError && (
<Image
src={startup.logo}
alt={`${startup.name} logo`}
width={100}
height={100}
onError={() => setImageError(true)}
/>
)}
</div>

<p className="text-xl mb-8">{startup.description}</p>

<h2 className="text-2xl font-semibold mb-4">Metrics</h2>
<ul className="mb-8">
{Object.entries(startup.metrics).map(([key, value]) => (
<li key={key} className="mb-2">
<span className="font-bold">{key}:</span> {String(value)}
</li>
))}
</ul>

<h2 className="text-2xl font-semibold mb-4">About {startup.name}</h2>
<p className="mb-8">{startup.about}</p>

<h2 className="text-2xl font-semibold mb-4">Founders</h2>
<ul className="mb-8">
{startup.founders.map((founder) => (
<li key={founder.name} className="mb-2">
<span className="font-bold">{founder.name}</span> - {founder.role}
</li>
))}
</ul>

<Link
href={startup.website}
target="_blank"
rel="noopener noreferrer"
className="bg-yellow-500 text-black px-4 py-2 rounded-full font-semibold hover:bg-yellow-600 transition-colors"
>
Visit Website
</Link>
</div>
</div>
);
};

export default StartupDetails;
Loading
Loading