Skip to content
Merged
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
Binary file modified bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"d3-scale": "^4.0.2",
"d3-scale-chromatic": "^3.1.0",
"d3-selection": "^3.0.0",
"embla-carousel-react": "^8.5.2",
"framer-motion": "^12.6.0",
"lucide-react": "^0.484.0",
"next-themes": "^0.4.6",
Expand Down
Binary file added public/imageography-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/imageography-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/imageography-3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/imageography-4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed public/minified-js.jpg
Binary file not shown.
239 changes: 239 additions & 0 deletions src/components/ui/carousel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
import * as React from "react";
import useEmblaCarousel, {
type UseEmblaCarouselType,
} from "embla-carousel-react";
import { ArrowLeft, ArrowRight } from "lucide-react";

import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";

type CarouselApi = UseEmblaCarouselType[1];
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
type CarouselOptions = UseCarouselParameters[0];
type CarouselPlugin = UseCarouselParameters[1];

type CarouselProps = {
opts?: CarouselOptions;
plugins?: CarouselPlugin;
orientation?: "horizontal" | "vertical";
setApi?: (api: CarouselApi) => void;
};

type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
api: ReturnType<typeof useEmblaCarousel>[1];
scrollPrev: () => void;
scrollNext: () => void;
canScrollPrev: boolean;
canScrollNext: boolean;
} & CarouselProps;

const CarouselContext = React.createContext<CarouselContextProps | null>(null);

function useCarousel() {
const context = React.useContext(CarouselContext);

if (!context) {
throw new Error("useCarousel must be used within a <Carousel />");
}

return context;
}

function Carousel({
orientation = "horizontal",
opts,
setApi,
plugins,
className,
children,
...props
}: React.ComponentProps<"div"> & CarouselProps) {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
axis: orientation === "horizontal" ? "x" : "y",
},
plugins,
);
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
const [canScrollNext, setCanScrollNext] = React.useState(false);

const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) return;
setCanScrollPrev(api.canScrollPrev());
setCanScrollNext(api.canScrollNext());
}, []);

const scrollPrev = React.useCallback(() => {
api?.scrollPrev();
}, [api]);

const scrollNext = React.useCallback(() => {
api?.scrollNext();
}, [api]);

const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
event.preventDefault();
scrollPrev();
} else if (event.key === "ArrowRight") {
event.preventDefault();
scrollNext();
}
},
[scrollPrev, scrollNext],
);

React.useEffect(() => {
if (!api || !setApi) return;
setApi(api);
}, [api, setApi]);

React.useEffect(() => {
if (!api) return;
onSelect(api);
api.on("reInit", onSelect);
api.on("select", onSelect);

return () => {
api?.off("select", onSelect);
};
}, [api, onSelect]);

return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation:
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}
>
<div
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
data-slot="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
);
}

function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
const { carouselRef, orientation } = useCarousel();

return (
<div
ref={carouselRef}
className="overflow-hidden"
data-slot="carousel-content"
>
<div
className={cn(
"flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className,
)}
{...props}
/>
</div>
);
}

function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
const { orientation } = useCarousel();

return (
<div
role="group"
aria-roledescription="slide"
data-slot="carousel-item"
className={cn(
"min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4",
className,
)}
{...props}
/>
);
}

function CarouselPrevious({
className,
variant = "outline",
size = "icon",
...props
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollPrev, canScrollPrev } = useCarousel();

return (
<Button
data-slot="carousel-previous"
variant={variant}
size={size}
className={cn(
"absolute size-8 rounded-full",
orientation === "horizontal"
? "top-1/2 -left-12 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className,
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ArrowLeft />
<span className="sr-only">Previous slide</span>
</Button>
);
}

function CarouselNext({
className,
variant = "outline",
size = "icon",
...props
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollNext, canScrollNext } = useCarousel();

return (
<Button
data-slot="carousel-next"
variant={variant}
size={size}
className={cn(
"absolute size-8 rounded-full",
orientation === "horizontal"
? "top-1/2 -right-12 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className,
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ArrowRight />
<span className="sr-only">Next slide</span>
</Button>
);
}

export {
type CarouselApi,
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext,
};
59 changes: 53 additions & 6 deletions src/constants/project-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ export type ProjectLink = {
url: string;
};

export type Detail = {
headline: string;
body: Detail[] | string;
};

export type Project = {
title: string;
description: string;
details: Array<{
headline: string;
body: string;
}>;
details: Detail[];
techStack: string[];
href: string;
github?: string;
Expand All @@ -22,10 +24,55 @@ export const PROJECTS: Project[] = [
{
title: "Imageography",
description: "A mobile application built on the React-Native framework.",
techStack: ["React-Native", "Redux", "TypeScript", "Firebase"],
techStack: ["React-Native", "Expo", "Redux", "TypeScript", "Firebase"],
href: "/projects/imageography",
github: "https://github.com/haypho/imageography",
details: [],
details: [
{
headline: "Overview",
body: `
My first mobile app built on Expo (i.e., React Native) and Firebase that allows
users to store potential photoshoot and modeling locations on a map.
The app was originally published to the Apple App Store and Google Play Store.
It has since been removed as I work towards launching a new app!
`,
},
{
headline: "Learnings",
body: [
{
headline: "Learning #1: A good app name makes all the difference",
body: `
Creating an app name gives you the chance to be creative and have fun
discussions with friends and family. The app name "Imageography" came
from a play on words (i.e., "image" and "geography"). Although I thought
the app name was pretty clever, I quickly realized how bad the name actually
was when trying to share it with potential users. Five sylables, difficult to
spell, too long to search in the app store, too similar to other "image" based
apps, etc. The list keeps going for this mistake. I probably should have
realized this issue sooner in conversations, but most of my time was spent
actually building the thing!
`,
},
{
headline: "Learning #2: Online vs Offline Apps",
body: `
The plan from the beginning was to allow the users to store locations on their
devices. I compromised with online capabilities to reduce cost initially, but
little did I know that this would be the downfall of the app almost instantly.
In today's world, everything is online. I know this, you know this, and the
users definitely expected this. When I lauched V1 of the app, the very first
question every user had for me was "How do I share this location with my friends?"
with the second one being "How do I get these markers on my other device?". The
expectation for online presence was overwhelming... I attempted to pivot to an
online version of the app via Firebase, but I was working on this project during
the nights/weekends. Each day I worked on a new online feature, I could see the
users leaving the app. This ultimately led me to remove the app from the app store.
`,
},
],
},
],
},
{
title: "React-Native Web Cache",
Expand Down
35 changes: 35 additions & 0 deletions src/features/projects/project-detail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Detail } from "@/constants";
import { cn } from "@/lib/utils";
import { ReactNode } from "react";

export type ProjectDetailProps = Detail & {
heading?: number;
children?: ReactNode;
};

export function ProjectDetail({ heading = 2, ...props }: ProjectDetailProps) {
return (
<>
<h2
className={cn("font-bold", {
"text-3xl": heading === 1,
"text-2xl": heading === 2,
"text-xl": heading === 3,
"text-lg": heading === 4,
"text-md": heading === 5,
"text-sm": heading === 6,
})}
>
{props.headline}
</h2>
{typeof props.body === "string" ? (
<p>{props.body}</p>
) : (
props.body.map((detail, index) => (
<ProjectDetail key={index} {...detail} heading={heading + 1} />
))
)}
{props.children}
</>
);
}
Loading