diff --git a/package-lock.json b/package-lock.json index 5002985..d04ef9c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@tailwindcss/vite": "^4.0.9", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-multi-carousel": "^2.8.5", "react-scroll-parallax": "^3.4.5", "tailwindcss": "^4.0.9" }, @@ -4748,6 +4749,15 @@ "react": "^19.0.0" } }, + "node_modules/react-multi-carousel": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/react-multi-carousel/-/react-multi-carousel-2.8.5.tgz", + "integrity": "sha512-C5DAvJkfzR2JK9YixZ3oyF9x6R4LW6nzTpIXrl9Oujxi4uqP9SzVVCjl+JLM3tSdqdjAx/oWZK3dTVBSR73Q+w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", diff --git a/package.json b/package.json index 041ee3e..c09d19d 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@tailwindcss/vite": "^4.0.9", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-multi-carousel": "^2.8.5", "react-scroll-parallax": "^3.4.5", "tailwindcss": "^4.0.9" }, diff --git a/src/App.css b/src/App.css deleted file mode 100644 index e69de29..0000000 diff --git a/src/App.tsx b/src/App.tsx index 2f88da8..adf8b08 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,11 +1,14 @@ import { useEffect, useRef, useState } from 'react'; -import './App.css'; import { About, + Contact, + Gallery, Header, HowWeWork, Presentation, + Projects, SectionNav, + Services, WhatWeDo, Why, } from './components'; @@ -14,6 +17,14 @@ import { ScrollableContext } from './contexts'; function App() { const sectionsDiv = useRef<HTMLDivElement>(null); const [elements, setElements] = useState<Element[]>([]); + const [scrollOffsetTop, setScrollOffsetTop] = useState<number>(0); + + useEffect(() => { + const onScroll = () => setScrollOffsetTop(window.scrollY); + window.removeEventListener('scroll', onScroll); + window.addEventListener('scroll', onScroll, { passive: true }); + return () => window.removeEventListener('scroll', onScroll); + }, []); useEffect(() => { if (sectionsDiv?.current) { @@ -28,20 +39,24 @@ function App() { }, [sectionsDiv?.current]); const scrollToElement = (element: Element) => { - element.scrollIntoView({ behavior: 'smooth' }); + element.scrollIntoView({ behavior: 'smooth', block: 'center' }); }; return ( <ScrollableContext.Provider value={{ elements, scrollToElement }}> - <div className="w-screen px-[5%]"> + <div className="w-screen"> <Header /> - <SectionNav /> + <SectionNav offsetTop={scrollOffsetTop} /> <div ref={sectionsDiv}> <Presentation /> <About /> <WhatWeDo /> <Why /> <HowWeWork /> + <Projects /> + <Services /> + <Gallery /> + <Contact /> </div> </div> </ScrollableContext.Provider> diff --git a/src/assets/A_Place_To_Be.jpg b/src/assets/A_Place_To_Be.jpg new file mode 100644 index 0000000..05bc75e Binary files /dev/null and b/src/assets/A_Place_To_Be.jpg differ diff --git a/src/assets/Bitcoin_Atlantis.jpg b/src/assets/Bitcoin_Atlantis.jpg new file mode 100644 index 0000000..293203e Binary files /dev/null and b/src/assets/Bitcoin_Atlantis.jpg differ diff --git a/src/components/contact.tsx b/src/components/contact.tsx new file mode 100644 index 0000000..4abbcc9 --- /dev/null +++ b/src/components/contact.tsx @@ -0,0 +1,23 @@ +import { IconMailFilled } from '@tabler/icons-react'; + +export function Contact() { + return ( + <div id="about" className="my-60 flex justify-center"> + <div className="w-[40rem] h-full flex flex-col items-left justify-center gap-6"> + <div className="flex flex-col gap-6"> + <strong className="text-7xl">Request a quote</strong> + <span className="text-2xl"> + Feel free to ask us any questions related to our activities. We will + be happy to answer you. + </span> + </div> + <div className="text-4xl leading-[2.3rem] flex items-center gap-4"> + <strong>hello@teamtoxic.xyz</strong> + <div className="flex items-center justify-center bg-white w-[4rem] aspect-square rounded-full"> + <IconMailFilled size={24} color="#BB2464" /> + </div> + </div> + </div> + </div> + ); +} diff --git a/src/components/gallery.css b/src/components/gallery.css new file mode 100644 index 0000000..b5b65c9 --- /dev/null +++ b/src/components/gallery.css @@ -0,0 +1,35 @@ +.custom-dot-list-style { + margin-bottom: 2rem; +} + +.custom-dot-list-style + .react-multi-carousel-dot.react-multi-carousel-dot--active + button { + width: 1.2rem; + height: 1.2rem; + background-color: white; + border: none; + opacity: 1; +} + +.custom-dot-list-style .react-multi-carousel-dot button { + width: 0.6rem; + height: 0.6rem; + background-color: white; + opacity: 0.6; + border: none; + margin-right: 1rem; +} + +.custom-carousel-container .react-multiple-carousel__arrow { + width: 3rem; + height: 3rem; + bottom: 2.5%; + border: thin solid gray; +} +.custom-carousel-container .react-multiple-carousel__arrow--right { + left: 20%; +} +.custom-carousel-container .react-multiple-carousel__arrow--left { + left: 15%; +} diff --git a/src/components/gallery.tsx b/src/components/gallery.tsx new file mode 100644 index 0000000..30fcd46 --- /dev/null +++ b/src/components/gallery.tsx @@ -0,0 +1,65 @@ +import Carousel from 'react-multi-carousel'; +import 'react-multi-carousel/lib/styles.css'; +import aPlaceToBe from '../assets/A_Place_To_Be.jpg'; +import bitcoinAtlantis from '../assets/Bitcoin_Atlantis.jpg'; +import './gallery.css'; + +export function Gallery() { + const gallery = [ + { + title: 'Event branding', + description: 'Bitcoin Atlantis', + bgImage: bitcoinAtlantis, + }, + { + title: 'Brand identity', + description: 'A Place To Be', + bgImage: aPlaceToBe, + }, + ]; + + const responsive = { + desktop: { + breakpoint: { max: 3000, min: 1024 }, + items: 1, + }, + tablet: { + breakpoint: { max: 1024, min: 464 }, + items: 1, + }, + mobile: { + breakpoint: { max: 464, min: 0 }, + items: 1, + }, + }; + return ( + <Carousel + className="w-[100vw] h-[90vh] mb-20" + swipeable={false} + draggable={false} + showDots={true} + responsive={responsive} + ssr={false} + infinite={true} + autoPlay={false} + keyBoardControl={true} + customTransition="all .5" + transitionDuration={500} + dotListClass="custom-dot-list-style" + containerClass="custom-carousel-container" + > + {gallery.map((item) => ( + <div + key={`gallery-${item.title}`} + className="relative w-100% h-[90%] flex flex-col items-center justify-center" + > + <img src={item.bgImage} className="absolute opacity-60 z-[-1]" /> + <div className="w-[70%] flex flex-col gap-2"> + <strong className="text-5xl">{item.title}</strong> + <strong className="text-7xl">{item.description}</strong> + </div> + </div> + ))} + </Carousel> + ); +} diff --git a/src/components/header.tsx b/src/components/header.tsx index 3ce8421..3cb7be6 100644 --- a/src/components/header.tsx +++ b/src/components/header.tsx @@ -2,7 +2,7 @@ import toxic_square from '../assets/toxic_square.png'; export function Header() { return ( - <div className="fixed w-[90%] flex pt-8 justify-between items-center"> + <div className="fixed w-[90%] flex pt-8 ml-[5%] justify-between items-center"> <div className="flex items-center gap-1"> <img src={toxic_square} className="w-[4rem] aspect-square" /> <div className="flex flex-col text-left"> diff --git a/src/components/index.ts b/src/components/index.ts index 9070aac..cf9c8e7 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,7 +1,11 @@ export * from './about'; +export * from './contact'; +export * from './gallery'; export * from './header'; export * from './how-we-work'; export * from './presentation'; +export * from './projects'; export * from './section-nav'; +export * from './services'; export * from './what-we-do'; export * from './why'; diff --git a/src/components/nav-dot.tsx b/src/components/nav-dot.tsx index a501b7d..dd7f902 100644 --- a/src/components/nav-dot.tsx +++ b/src/components/nav-dot.tsx @@ -12,7 +12,7 @@ export function NavDot({ return ( <div className={ - 'w-[1rem] aspect-square rounded-full flex items-center justify-center cursor-pointer z-1' + + 'w-[1rem] aspect-square rounded-full flex items-center justify-center cursor-pointer' + (isActive || isHovering ? ' bg-stone-600' : '') } onClick={onClick} diff --git a/src/components/presentation.tsx b/src/components/presentation.tsx index e1dac70..19b520f 100644 --- a/src/components/presentation.tsx +++ b/src/components/presentation.tsx @@ -11,7 +11,7 @@ export function Presentation() { }; return ( - <div id="presentation" className="flex flex-col justify-end h-[100vh] pb-8"> + <div className="flex flex-col mx-[5%] justify-end h-[100vh] pb-8"> <div className="flex items-end"> <div className="text-7xl w-min text-left">Team Toxic</div> <div className="flex items-end px-4 grow"> diff --git a/src/components/project.tsx b/src/components/project.tsx new file mode 100644 index 0000000..d5a2d27 --- /dev/null +++ b/src/components/project.tsx @@ -0,0 +1,18 @@ +export type ProjectProps = { + company: string; + job: string; + bgImage: string; +}; + +export function Project({ company, job, bgImage }: ProjectProps) { + return ( + <div + className={`flex flex-col text-right justify-end h-[35rem] bg-${bgImage}`} + > + <div className="flex flex-col gap-5 mr-[-2rem]"> + <strong className="text-4xl">{company}</strong> + <strong className="text-2xl opacity-80 pb-15">{job}</strong> + </div> + </div> + ); +} diff --git a/src/components/projects.tsx b/src/components/projects.tsx new file mode 100644 index 0000000..a17446d --- /dev/null +++ b/src/components/projects.tsx @@ -0,0 +1,61 @@ +import { Project, ProjectProps } from './project'; + +export function Projects() { + const projects: ProjectProps[] = [ + { + company: 'Bitcoin Atlantis', + job: 'Conference branding', + bgImage: '', + }, + { + company: 'A Place To Be', + job: 'Brand development', + bgImage: '', + }, + { + company: 'Sovereign Engineering', + job: 'Visual identity', + bgImage: '', + }, + { + company: 'BitcoinWalk', + job: 'Logo design', + bgImage: '', + }, + { + company: 'Freedom Tech Co.', + job: 'Brand development', + bgImage: '', + }, + ]; + + return ( + <div className="my-20 w-full flex flex-col items-center gap-20"> + <div className="w-[50rem] flex flex-col gap-6"> + <strong className="text-6xl">Projects</strong> + <span className="text-xl"> + Our portfolio only showcases projects and products that are centered + around Bitcoin and nostr protocols. The work we did in the fiat world + before gave us the experience but from now on we take pride in our + Bitcoin work only. + </span> + </div> + <div className="w-[70%] flex gap-10"> + <div className="mt-20 w-[50%] flex flex-col items-end"> + {projects + .filter((_, i) => i % 2 !== 0) + .map((p) => ( + <Project key={`Project-${p.company}-${p.job}`} {...p} /> + ))} + </div> + <div className="w-[50%] flex flex-col items-end"> + {projects + .filter((_, i) => i % 2 === 0) + .map((p) => ( + <Project key={`Project-${p.company}-${p.job}`} {...p} /> + ))} + </div> + </div> + </div> + ); +} diff --git a/src/components/section-nav.tsx b/src/components/section-nav.tsx index 649e1fc..7c696a3 100644 --- a/src/components/section-nav.tsx +++ b/src/components/section-nav.tsx @@ -1,26 +1,27 @@ import { useEffect, useState } from 'react'; -import { useScroll } from '../hooks'; import { NavDot } from './nav-dot'; import { useScrollableContext } from '../contexts'; -export function SectionNav() { - const { observeElements } = useScroll(); +export function SectionNav({ offsetTop }: { offsetTop: number }) { const scrollableContext = useScrollableContext(); - const [sections, setSections] = useState<Element[]>([]); - const [selectedSection, setSelectedSection] = useState<Element>(); + const [sections, setSections] = useState<HTMLElement[]>([]); + const [selectedSection, setSelectedSection] = useState<HTMLElement>(); useEffect(() => { - setSections(scrollableContext.elements ?? []); + setSections(scrollableContext.elements.map((e) => e as HTMLElement) ?? []); }, [scrollableContext.elements]); useEffect(() => { - if (sections.length > 0) { - observeElements(sections, setSelectedSection); - } - }, [sections]); + const offset = offsetTop + window.screen.height / 3; + + const newSelectedSection = + sections.find((s) => offset < s.offsetTop + s.offsetHeight) ?? + sections[sections.length - 1]; + setSelectedSection(newSelectedSection); + }, [sections, offsetTop]); return ( - <div className="fixed right-[5%] top-[45%] flex flex-col"> + <div className="fixed right-[5%] top-[45%] flex flex-col z-1"> {sections.map((s, index) => ( <NavDot key={`nav-dot-${index}`} diff --git a/src/components/services.tsx b/src/components/services.tsx new file mode 100644 index 0000000..dea80dc --- /dev/null +++ b/src/components/services.tsx @@ -0,0 +1,25 @@ +export function Services() { + const services = ['Brand identity', 'Logo Design', 'Brand strategy']; + + return ( + <div className="my-20 w-full flex flex-col items-center gap-20"> + <div className="w-[50rem] flex flex-col gap-6"> + <strong className="text-6xl">Services</strong> + </div> + <div className="w-[50rem] flex flex-wrap "> + {services.map((service, i) => ( + <div + key={`services-${service}`} + className="h-[20rem] flex flex-col items-start justify-center gap-2" + > + <strong className="text-4xl opacity-40"> + {(i + 1).toLocaleString('en-US', { minimumIntegerDigits: 2 })} + </strong> + <strong className="w-[50%] text-6xl my-2">{service}</strong> + <span className="text-lg border-t-2 pt-1">Learn more</span> + </div> + ))} + </div> + </div> + ); +} diff --git a/src/hooks/index.ts b/src/hooks/index.ts deleted file mode 100644 index fdf68e6..0000000 --- a/src/hooks/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './useScroll'; diff --git a/src/hooks/useScroll.tsx b/src/hooks/useScroll.tsx deleted file mode 100644 index b7d5bca..0000000 --- a/src/hooks/useScroll.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { useState } from 'react'; - -export function useScroll() { - const [observer, setObserver] = useState<IntersectionObserver>(); - - const observeElements = ( - elements: Element[], - cb: (elm: Element) => void - ): void => { - if (observer != null) { - observer.disconnect(); - } - - const threshold = 0.4; - const newObserver = new IntersectionObserver( - (entries) => { - const entry = entries[0]; - if (entry.isIntersecting) cb(entry.target); - }, - { threshold } - ); - - elements.forEach((e) => newObserver.observe(e)); - setObserver(observer); - }; - - return { - observeElements, - }; -}