parent
4c473a01c6
commit
5a1f69fd1f
10
package-lock.json
generated
10
package-lock.json
generated
@ -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",
|
||||
|
@ -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"
|
||||
},
|
||||
|
23
src/App.tsx
23
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>
|
||||
|
BIN
src/assets/A_Place_To_Be.jpg
Normal file
BIN
src/assets/A_Place_To_Be.jpg
Normal file
Binary file not shown.
After ![]() (image error) Size: 536 KiB |
BIN
src/assets/Bitcoin_Atlantis.jpg
Normal file
BIN
src/assets/Bitcoin_Atlantis.jpg
Normal file
Binary file not shown.
After ![]() (image error) Size: 756 KiB |
23
src/components/contact.tsx
Normal file
23
src/components/contact.tsx
Normal file
@ -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>
|
||||
);
|
||||
}
|
35
src/components/gallery.css
Normal file
35
src/components/gallery.css
Normal file
@ -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%;
|
||||
}
|
65
src/components/gallery.tsx
Normal file
65
src/components/gallery.tsx
Normal file
@ -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>
|
||||
);
|
||||
}
|
@ -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">
|
||||
|
@ -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';
|
||||
|
@ -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}
|
||||
|
@ -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">
|
||||
|
18
src/components/project.tsx
Normal file
18
src/components/project.tsx
Normal file
@ -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>
|
||||
);
|
||||
}
|
61
src/components/projects.tsx
Normal file
61
src/components/projects.tsx
Normal file
@ -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>
|
||||
);
|
||||
}
|
@ -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}`}
|
||||
|
25
src/components/services.tsx
Normal file
25
src/components/services.tsx
Normal file
@ -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>
|
||||
);
|
||||
}
|
@ -1 +0,0 @@
|
||||
export * from './useScroll';
|
@ -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,
|
||||
};
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user