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
27 changes: 26 additions & 1 deletion app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -339,4 +339,29 @@
body {
@apply bg-background text-foreground;
}
}
}

/* react-tweet theme overrides */
:root .react-tweet-theme {
--tweet-bg-color: #F0EFEA;
--tweet-bg-color-hover: #EBEAE5;
--tweet-quoted-bg-color-hover: rgba(0, 0, 0, 0.03);
--tweet-border: 1px solid hsl(45 8% 80%);
--tweet-font-color: #26251E;
--tweet-font-color-secondary: #3B3A33;
--tweet-color-blue-primary: #F54E00;
--tweet-color-blue-primary-hover: #dc4600;
--tweet-font-family: inherit;
}

.dark .react-tweet-theme {
--tweet-bg-color: #1B1913;
--tweet-bg-color-hover: #201E18;
--tweet-quoted-bg-color-hover: rgba(255, 255, 255, 0.03);
--tweet-border: 1px solid hsl(40 8% 25%);
--tweet-font-color: #EDECEC;
--tweet-font-color-secondary: #969592;
--tweet-color-blue-primary: #F54E00;
--tweet-color-blue-primary-hover: #dc4600;
--tweet-font-family: inherit;
}
25 changes: 14 additions & 11 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import "./globals.css"
import { ThemeProvider } from "@/components/providers/theme-provider"
import { NavigationProvider } from "@/components/providers/navigation-provider"
import { StickyTitleProvider } from "@/components/providers/sticky-title-provider"
import { Toaster } from "@/components/ui/sonner"
import { Montserrat, Instrument_Serif } from "next/font/google"
import { TopNav } from "@/components/layout/top-nav"
Expand Down Expand Up @@ -81,17 +82,19 @@ export default function RootLayout({
enableSystem
>
<NavigationProvider>
{process.env.VERCEL_GIT_COMMIT_REF === "dev" && (
<div className="fixed top-2 left-2 z-[100] bg-accent text-white text-xs font-bold px-2 py-1 rounded">
dev
</div>
)}
<TopNav />
<main className="min-h-[calc(100vh-3.5rem)]">
<PageTransition>{children}</PageTransition>
</main>
<Toaster />
<GPTSlopToast />
<StickyTitleProvider>
{process.env.VERCEL_GIT_COMMIT_REF === "dev" && (
<div className="fixed top-2 left-2 z-[100] bg-accent text-white text-xs font-bold px-2 py-1 rounded">
dev
</div>
)}
<TopNav />
<main className="min-h-[calc(100vh-3.5rem)]">
<PageTransition>{children}</PageTransition>
</main>
<Toaster />
<GPTSlopToast />
</StickyTitleProvider>
</NavigationProvider>
</ThemeProvider>
<Analytics />
Expand Down
13 changes: 11 additions & 2 deletions app/projects/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { ComingSoon } from "@/components/common/coming-soon";
import { GithubContributions } from "@/components/common/github-calendar";
import { ProjectList } from "@/components/common/project-list";
import { PageWrapper } from "@/components/layout/page-wrapper";
import { Separator } from "@/components/ui/separator";
import { projects } from "@/content/projects-data";

export const metadata = {
title: 'projects',
Expand All @@ -9,7 +12,13 @@ export const metadata = {
export default function ProjectsPage(): React.ReactNode {
return (
<PageWrapper title="projects" subtitle="a showcase of my technical work.">
<ComingSoon />
<div className="space-y-8">
<Separator />
<GithubContributions />
<Separator />
<ProjectList projects={projects} />
<p className="text-sm text-muted-foreground text-center">and more...</p>
</div>
</PageWrapper>
)
}
44 changes: 41 additions & 3 deletions bun.lock

Large diffs are not rendered by default.

24 changes: 24 additions & 0 deletions components/common/github-calendar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"use client"

import { useState, useEffect } from "react"
import { GitHubCalendar } from "react-github-calendar"

export function GithubContributions() {
const [mounted, setMounted] = useState(false)

useEffect(() => {
setMounted(true)
}, [])

return (
<div className="space-y-4">
<p className="text-sm text-muted-foreground">
many of my company projects are under NDA and can&apos;t be shown here,
but my github contribution calendar gives an idea of my output.
</p>
<a href="https://github.com/jakebodea" target="_blank" rel="noopener noreferrer" className="block overflow-x-auto">
{mounted && <GitHubCalendar username="jakebodea" colorScheme="dark" />}
</a>
</div>
)
}
63 changes: 63 additions & 0 deletions components/common/project-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { ExternalLink, Github } from 'lucide-react'
import { Card, CardContent } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { XEmbed } from '@/components/common/x-embed'
import type { ProjectData } from '@/content/projects-data'

interface ProjectCardProps {
project: ProjectData
}

export function ProjectCard({ project }: ProjectCardProps) {
return (
<Card>
<CardContent className="p-6 space-y-4">
<h2 className="text-2xl font-serif text-foreground">{project.title}</h2>

<p className="text-sm text-muted-foreground">
{project.description}
</p>

<div className="flex flex-wrap gap-2">
{project.techStack.map((tech) => (
<Badge key={tech} variant="secondary">
{tech}
</Badge>
))}
</div>

<div className="flex gap-2">
{project.liveUrl && (
<Button variant="ghost" size="sm" asChild>
<a
href={project.liveUrl}
target="_blank"
rel="noopener noreferrer"
>
<ExternalLink />
Live site
</a>
</Button>
)}
{project.repoUrl && (
<Button variant="ghost" size="sm" asChild>
<a
href={project.repoUrl}
target="_blank"
rel="noopener noreferrer"
>
<Github />
Source
</a>
</Button>
)}
</div>

{project.media?.type === 'x-embed' && (
<XEmbed url={project.media.url} />
)}
</CardContent>
</Card>
)
}
16 changes: 16 additions & 0 deletions components/common/project-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { ProjectCard } from '@/components/common/project-card'
import type { ProjectData } from '@/content/projects-data'

interface ProjectListProps {
projects: ProjectData[]
}

export function ProjectList({ projects }: ProjectListProps) {
return (
<div className="space-y-8">
{projects.map((project) => (
<ProjectCard key={project.title} project={project} />
))}
</div>
)
}
21 changes: 21 additions & 0 deletions components/common/x-embed.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Tweet } from 'react-tweet'

interface XEmbedProps {
url: string
}

function extractTweetId(url: string): string | null {
const match = url.match(/status\/(\d+)/)
return match?.[1] ?? null
}

export function XEmbed({ url }: XEmbedProps) {
const tweetId = extractTweetId(url)
if (!tweetId) return null

return (
<div className="mt-4 flex justify-center [&>div]:!m-0">
<Tweet id={tweetId} />
</div>
)
}
29 changes: 28 additions & 1 deletion components/layout/page-wrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"use client"

import { useEffect, useRef, useState } from "react"
import { PageTitle } from "@/components/layout/page-title"
import { useStickyTitle } from "@/components/providers/sticky-title-provider"

interface PageWrapperProps {
title: string
Expand All @@ -9,10 +11,35 @@ interface PageWrapperProps {
}

export function PageWrapper({ title, subtitle, children }: PageWrapperProps) {
const { setHasStickyTitle } = useStickyTitle()
const sentinelRef = useRef<HTMLDivElement>(null)
const [isStuck, setIsStuck] = useState(false)

useEffect(() => {
setHasStickyTitle(true)
return () => setHasStickyTitle(false)
}, [setHasStickyTitle])

useEffect(() => {
const sentinel = sentinelRef.current
if (!sentinel) return

const observer = new IntersectionObserver(
([entry]) => setIsStuck(!entry.isIntersecting),
{ threshold: 0 }
)
observer.observe(sentinel)
return () => observer.disconnect()
}, [])

return (
<div className="min-h-full">
<div className="container mx-auto max-w-4xl px-6 py-8">
<div className="sticky top-0 z-50 bg-background -mx-6 px-6 h-14 flex items-center">
<div ref={sentinelRef} className="h-0" />
<div className="relative sticky top-0 z-50 bg-background -mx-6 px-6 h-14 flex items-center">
{isStuck && (
<div className="absolute bottom-0 left-0 right-0 h-16 translate-y-full bg-gradient-to-b from-background to-transparent pointer-events-none" />
)}
<PageTitle variant="page" className="!m-0">{title}</PageTitle>
</div>
{subtitle && (
Expand Down
9 changes: 7 additions & 2 deletions components/layout/top-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ import { TooltipProvider } from "@/components/ui/tooltip"
import { ShortcutTooltip } from "@/components/common/shortcut-tooltip"
import { MobileMenu } from "@/components/layout/mobile-menu"
import { ThemeToggle } from "@/components/layout/theme-toggle"
import { useStickyTitle } from "@/components/providers/sticky-title-provider"

const navItems = [
{ title: "home", href: "/" },
{ title: "timeline", href: "/timeline" },
{ title: "projects", href: "/projects" },
{ title: "timeline", href: "/timeline" },
{ title: "blogs", href: "/blogs" },
{ title: "quotes", href: "/quotes" },
{ title: "contact", href: "/contact" },
Expand All @@ -23,6 +24,7 @@ const navItems = [
export function TopNav() {
const pathname = usePathname()
const router = useRouter()
const { hasStickyTitle } = useStickyTitle()
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
const navContainerRef = useRef<HTMLDivElement>(null)
const [indicator, setIndicator] = useState({ left: 0, width: 0, opacity: 0 })
Expand Down Expand Up @@ -67,7 +69,10 @@ export function TopNav() {

return (
<>
<nav className="sticky top-0 z-[80] w-full bg-transparent">
<nav className={cn("sticky top-0 z-[80] w-full", hasStickyTitle ? "bg-transparent" : "bg-background")}>
{!hasStickyTitle && (
<div className="absolute bottom-0 left-0 right-0 h-16 translate-y-full bg-gradient-to-b from-background to-transparent pointer-events-none" />
)}
<div className="mx-auto max-w-4xl px-6">
<div className="flex h-14 items-center justify-center">
{/* Desktop Navigation */}
Expand Down
33 changes: 33 additions & 0 deletions components/providers/sticky-title-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"use client"

import { createContext, useContext, useState, useMemo } from "react"
import type { ReactNode } from "react"

interface StickyTitleContextType {
hasStickyTitle: boolean
setHasStickyTitle: (value: boolean) => void
}

const StickyTitleContext = createContext<StickyTitleContextType>({
hasStickyTitle: false,
setHasStickyTitle: () => {},
})

export function useStickyTitle() {
return useContext(StickyTitleContext)
}

export function StickyTitleProvider({ children }: { children: ReactNode }) {
const [hasStickyTitle, setHasStickyTitle] = useState(false)

const value = useMemo(
() => ({ hasStickyTitle, setHasStickyTitle }),
[hasStickyTitle]
)

return (
<StickyTitleContext.Provider value={value}>
{children}
</StickyTitleContext.Provider>
)
}
36 changes: 36 additions & 0 deletions components/ui/badge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/lib/utils"

const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-foreground/15 text-foreground hover:bg-foreground/20",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)

export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}

function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}

export { Badge, badgeVariants }
2 changes: 1 addition & 1 deletion components/ui/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const buttonVariants = cva(
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
ghost: "text-muted-foreground hover:text-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
Expand Down
Loading