Finished a first pass of the project section
This commit is contained in:
5
src/components/Container.tsx
Normal file
5
src/components/Container.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { PropsWithChildren } from "react";
|
||||
|
||||
export const Container: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
return <div className="container">{children}</div>;
|
||||
};
|
||||
@@ -1,4 +1,6 @@
|
||||
import Image from "next/image";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import useIntersectionObserver from "~/hooks/useIntersectionObserver";
|
||||
|
||||
export interface Props {
|
||||
title: string;
|
||||
@@ -7,6 +9,18 @@ export interface Props {
|
||||
}
|
||||
|
||||
export const Polaroid: React.FC<Props> = ({ title, imageSrc, rotation }) => {
|
||||
const [showImage, setShowImage] = useState(false);
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const entry = useIntersectionObserver(ref, {});
|
||||
|
||||
const isOnScreen = !!entry?.isIntersecting;
|
||||
|
||||
useEffect(() => {
|
||||
if (isOnScreen) {
|
||||
setShowImage(true);
|
||||
}
|
||||
}, [isOnScreen]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`m-5 w-52 rounded bg-[#F8F2EA] text-black shadow-md sm:w-64 lg:w-80 ${rotation}`}
|
||||
@@ -14,13 +28,20 @@ export const Polaroid: React.FC<Props> = ({ title, imageSrc, rotation }) => {
|
||||
<div className="flex h-full w-full flex-col items-center justify-between gap-y-3 p-4 sm:gap-y-4 sm:p-5 lg:gap-y-5 lg:p-6">
|
||||
<div className="relative aspect-square w-full">
|
||||
<Image
|
||||
src="https://picsum.photos/200"
|
||||
src={imageSrc}
|
||||
alt={`Showcase of ${title}`}
|
||||
fill
|
||||
className="object-cover shadow-inner"
|
||||
/>
|
||||
<div
|
||||
className={`absolute inset-0 transition-colors duration-1000 ${
|
||||
showImage ? "bg-transparent" : "bg-[#3D3D3D]"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-2xl font-semibold">{title}</span>
|
||||
<span ref={ref} className="text-2xl font-semibold">
|
||||
{title}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -12,8 +12,8 @@ export const ProjectDescription: React.FC<Props> = ({
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<div className="max-w-5xl text-black">
|
||||
<h2 className="text-3xl font-semibold">{title}</h2>
|
||||
<div className="max-w-4xl text-black">
|
||||
<h3 className="text-3xl font-semibold">{title}</h3>
|
||||
<Separator.Root
|
||||
className={`${color} my-2 h-1 w-full rounded-full`}
|
||||
decorative
|
||||
|
||||
@@ -10,6 +10,7 @@ const roadster = localFont({
|
||||
preload: true,
|
||||
variable: "--font-roadster",
|
||||
});
|
||||
|
||||
const jakarta = Plus_Jakarta_Sans({
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "600", "800"],
|
||||
|
||||
@@ -5,7 +5,7 @@ export const projects: Array<PolaroidProps & ProjectDescriptionProps> = [
|
||||
{
|
||||
title: "Parallel",
|
||||
color: "bg-orange",
|
||||
imageSrc: "https://unsplash.com/photos/OqtafYT5kTw",
|
||||
imageSrc: "https://picsum.photos/seed/Parallel/200",
|
||||
rotation: "-rotate-12",
|
||||
children:
|
||||
"We started Parallel to help connect educators with content creators to make it easier to create high-quality educational content for platforms like YouTube. We built the platform with high performance, scalability, and accessibility in mind. That's why we chose powerful tools like Next.js, TailwindCSS, RadixUI, Prisma, PlanetScale, and Vercel's hosting platform. ",
|
||||
@@ -13,15 +13,15 @@ export const projects: Array<PolaroidProps & ProjectDescriptionProps> = [
|
||||
{
|
||||
title: "DropNote",
|
||||
color: "bg-yellow",
|
||||
imageSrc: "",
|
||||
imageSrc: "https://picsum.photos/seed/DropNote/200",
|
||||
rotation: "rotate-6",
|
||||
children:
|
||||
"We started Parallel to help connect educators with content creators to make it easier to create high-quality educational content for platforms like YouTube. We built the platform with high performance, scalability, and accessibility in mind. That's why we chose powerful tools like Next.js, TailwindCSS, RadixUI, Prisma, PlanetScale, and Vercel's hosting platform. ",
|
||||
},
|
||||
{
|
||||
title: "Flurry",
|
||||
title: "Flurry Waitlist",
|
||||
color: "bg-green",
|
||||
imageSrc: "",
|
||||
imageSrc: "https://picsum.photos/seed/Flurry/200",
|
||||
rotation: "-rotate-3",
|
||||
children:
|
||||
"We started Parallel to help connect educators with content creators to make it easier to create high-quality educational content for platforms like YouTube. We built the platform with high performance, scalability, and accessibility in mind. That's why we chose powerful tools like Next.js, TailwindCSS, RadixUI, Prisma, PlanetScale, and Vercel's hosting platform. ",
|
||||
|
||||
45
src/hooks/useIntersectionObserver.ts
Normal file
45
src/hooks/useIntersectionObserver.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { type RefObject, useEffect, useState } from "react";
|
||||
|
||||
interface Args extends IntersectionObserverInit {
|
||||
freezeOnceVisible?: boolean;
|
||||
}
|
||||
|
||||
function useIntersectionObserver(
|
||||
elementRef: RefObject<Element>,
|
||||
{
|
||||
threshold = 0,
|
||||
root = null,
|
||||
rootMargin = "0%",
|
||||
freezeOnceVisible = false,
|
||||
}: Args
|
||||
): IntersectionObserverEntry | undefined {
|
||||
const [entry, setEntry] = useState<IntersectionObserverEntry>();
|
||||
|
||||
const frozen = entry?.isIntersecting && freezeOnceVisible;
|
||||
|
||||
const updateEntry = ([entry]: IntersectionObserverEntry[]): void => {
|
||||
setEntry(entry);
|
||||
};
|
||||
|
||||
const stringifiedThreshold = JSON.stringify(threshold);
|
||||
|
||||
useEffect(() => {
|
||||
const node = elementRef?.current; // DOM Ref
|
||||
const hasIOSupport = !!window.IntersectionObserver;
|
||||
|
||||
if (!hasIOSupport || frozen || !node) return;
|
||||
|
||||
const observerParams = { threshold, root, rootMargin };
|
||||
const observer = new IntersectionObserver(updateEntry, observerParams);
|
||||
|
||||
observer.observe(node);
|
||||
|
||||
return () => observer.disconnect();
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [elementRef?.current, stringifiedThreshold, root, rootMargin, frozen]);
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
export default useIntersectionObserver;
|
||||
@@ -1,5 +1,6 @@
|
||||
import { type NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import { Container } from "~/components/Container";
|
||||
import { Polaroid } from "~/components/Polaroid";
|
||||
import { ProjectDescription } from "~/components/ProjectDescription";
|
||||
import { projects } from "~/constants/projects";
|
||||
@@ -43,27 +44,32 @@ const Hero = () => {
|
||||
|
||||
const Projects = () => {
|
||||
return (
|
||||
<div className="container py-20 [&>*:nth-child(odd)]:md:flex-row">
|
||||
{projects.map((p) => (
|
||||
<div
|
||||
key={p.title}
|
||||
className="my-20 flex flex-col items-center gap-x-12 gap-y-6 md:my-0 md:flex-row-reverse"
|
||||
>
|
||||
<div className="">
|
||||
<Polaroid
|
||||
title={p.title}
|
||||
imageSrc={p.imageSrc}
|
||||
rotation={p.rotation}
|
||||
/>
|
||||
<Container>
|
||||
<h2 className="text-center text-4xl font-extrabold text-black">
|
||||
Here are some of our past projects
|
||||
</h2>
|
||||
<div className="py-20 [&>*:nth-child(odd)]:md:flex-row">
|
||||
{projects.map((p) => (
|
||||
<div
|
||||
key={p.title}
|
||||
className="my-20 flex flex-col items-center gap-x-12 gap-y-6 md:my-0 md:flex-row-reverse"
|
||||
>
|
||||
<div>
|
||||
<Polaroid
|
||||
title={p.title}
|
||||
imageSrc={p.imageSrc}
|
||||
rotation={p.rotation}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<ProjectDescription title={p.title} color={p.color}>
|
||||
{p.children}
|
||||
</ProjectDescription>
|
||||
</div>
|
||||
</div>
|
||||
<div className="">
|
||||
<ProjectDescription title={p.title} color={p.color}>
|
||||
{p.children}
|
||||
</ProjectDescription>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user