diff --git a/src/components/Icon.astro b/src/components/Icon.astro index d8c8b89..b8fb0ba 100644 --- a/src/components/Icon.astro +++ b/src/components/Icon.astro @@ -44,7 +44,7 @@ import { Globe, Mic, Clock, Webhook, CloudSun, Image, Camera, Search, Mail, StickyNote, CheckSquare, ListTodo, PenTool, Bed, ShoppingCart, Printer, Heart, UtensilsCrossed, MonitorSmartphone, Eye, Monitor, Bot, Users, Hash, - Home, MessageCircle, Brain, Terminal, Puzzle, Zap + Home, MessageCircle, Brain, Terminal, Puzzle, Zap, Calendar, BookOpen } from '@lucide/astro'; const { icon, color = 'currentColor', size = 24, class: className = '' } = Astro.props; @@ -81,6 +81,8 @@ const lucideIcons: Record = { 'lucide:terminal': Terminal, 'lucide:puzzle': Puzzle, 'lucide:zap': Zap, + 'lucide:calendar': Calendar, + 'lucide:book-open': BookOpen, }; const isStringIcon = typeof icon === 'string'; diff --git a/src/components/LobsterLogo.astro b/src/components/LobsterLogo.astro new file mode 100644 index 0000000..905eedf --- /dev/null +++ b/src/components/LobsterLogo.astro @@ -0,0 +1,57 @@ +--- +interface Props { + animated?: boolean; + gradientId?: string; +} + +const { animated = false, gradientId = 'lobster-gradient' } = Astro.props; + +const animatedClass = (base: string) => (animated ? base : undefined); +--- + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/Navbar.astro b/src/components/Navbar.astro new file mode 100644 index 0000000..84c2a35 --- /dev/null +++ b/src/components/Navbar.astro @@ -0,0 +1,337 @@ +--- +import Icon from './Icon.astro'; +import LobsterLogo from './LobsterLogo.astro'; +import { siGithub } from 'simple-icons'; + +const siIcon = (icon: any) => icon.path; +--- + + + + diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro index 6a4bc6c..164f6c1 100644 --- a/src/layouts/Layout.astro +++ b/src/layouts/Layout.astro @@ -43,10 +43,10 @@ const { - - - - + + + + {title} @@ -54,122 +54,140 @@ const { (() => { const key = 'oc-theme'; const saved = localStorage.getItem(key); - const theme = saved === 'light' || saved === 'dark' ? saved : 'dark'; + const preference = saved === 'light' || saved === 'dark' || saved === 'system' ? saved : 'system'; + + const getEffectiveTheme = (pref) => { + if (pref === 'system') { + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + } + return pref; + }; + + const theme = getEffectiveTheme(preference); document.documentElement.dataset.theme = theme; + document.documentElement.dataset.themePreference = preference; document.documentElement.style.colorScheme = theme; })(); - @@ -278,29 +278,104 @@ const { (() => { const key = 'oc-theme'; const root = document.documentElement; - const toggle = document.querySelector('[data-theme-toggle]'); - const icon = document.querySelector('[data-theme-icon]'); - if (!toggle || !icon) return; - - const applyTheme = (theme, persist = true) => { - root.dataset.theme = theme; - root.style.colorScheme = theme; - const isDark = theme === 'dark'; - icon.textContent = isDark ? '☀' : '☾'; - toggle.setAttribute('aria-label', isDark ? 'Switch to light mode' : 'Switch to dark mode'); - toggle.setAttribute('title', isDark ? 'Switch to light mode' : 'Switch to dark mode'); - if (persist) { - localStorage.setItem(key, theme); + const buttons = document.querySelectorAll('[data-theme-btn]'); + if (!buttons.length) return; + + const getEffectiveTheme = (preference) => { + if (preference === 'system') { + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + } + return preference; + }; + + const cleanupTransition = () => { + root.classList.remove('theme-transition'); + root.style.removeProperty('--theme-switch-x'); + root.style.removeProperty('--theme-switch-y'); + }; + + const applyTheme = (preference, context = null, persist = true) => { + const currentPreference = root.dataset.themePreference; + if (currentPreference === preference) return; + + const effectiveTheme = getEffectiveTheme(preference); + const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + + const updateTheme = () => { + root.dataset.theme = effectiveTheme; + root.dataset.themePreference = preference; + root.style.colorScheme = effectiveTheme; + + // Update all theme button states (fixed + navbar) + const allButtons = document.querySelectorAll('[data-theme-btn]'); + allButtons.forEach(btn => { + const btnTheme = btn.getAttribute('data-theme-btn'); + if (btnTheme === preference) { + btn.classList.add('active'); + } else { + btn.classList.remove('active'); + } + }); + + if (persist) { + localStorage.setItem(key, preference); + } + }; + + // Use View Transitions API if available and not reduced motion + if (!prefersReducedMotion && document.startViewTransition && context) { + const rect = context.getBoundingClientRect(); + const x = (rect.left + rect.width / 2) / window.innerWidth * 100; + const y = (rect.top + rect.height / 2) / window.innerHeight * 100; + + root.style.setProperty('--theme-switch-x', `${x}%`); + root.style.setProperty('--theme-switch-y', `${y}%`); + root.classList.add('theme-transition'); + + const transition = document.startViewTransition(() => { + updateTheme(); + }); + + transition.finished.then(() => { + cleanupTransition(); + }).catch(() => { + cleanupTransition(); + }); + } else { + updateTheme(); + cleanupTransition(); } }; - toggle.addEventListener('click', () => { - const next = root.dataset.theme === 'light' ? 'dark' : 'light'; - applyTheme(next); + // Listen for button clicks (desktop + navbar) + const allThemeButtons = document.querySelectorAll('[data-theme-btn]'); + allThemeButtons.forEach(btn => { + btn.addEventListener('click', (e) => { + const preference = btn.getAttribute('data-theme-btn'); + applyTheme(preference, e.currentTarget); + }); }); - const current = root.dataset.theme === 'light' ? 'light' : 'dark'; - applyTheme(current, false); + // Listen for system theme changes when in system mode + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + mediaQuery.addEventListener('change', () => { + const currentPreference = root.dataset.themePreference; + if (currentPreference === 'system') { + applyTheme('system', null, false); + } + }); + + // Initialize button states on page load + const currentPreference = root.dataset.themePreference || 'system'; + const allButtons = document.querySelectorAll('[data-theme-btn]'); + allButtons.forEach(btn => { + const btnTheme = btn.getAttribute('data-theme-btn'); + if (btnTheme === currentPreference) { + btn.classList.add('active'); + } else { + btn.classList.remove('active'); + } + }); })(); diff --git a/src/pages/index.astro b/src/pages/index.astro index 0b32ece..bf7641c 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,6 +1,8 @@ --- import Layout from '../layouts/Layout.astro'; +import Navbar from '../components/Navbar.astro'; import Icon from '../components/Icon.astro'; +import LobsterLogo from '../components/LobsterLogo.astro'; import testimonials from '../data/testimonials.json'; import { getPublishedBlogPosts } from '../lib/blog'; @@ -8,8 +10,8 @@ import { getPublishedBlogPosts } from '../lib/blog'; const [latestPost] = await getPublishedBlogPosts(); import { siWhatsapp, siTelegram, siDiscord, siSignal, siApple, - siAnthropic, siSpotify, siObsidian, siGithub, siGooglechrome, siGmail, - siX, siPhilipshue + siAnthropic, siGithub, siGooglechrome, siGmail, + siX, siNotion, siLinear, siTrello, siZoom } from 'simple-icons'; const siIcon = (icon: any) => icon.path; @@ -23,14 +25,11 @@ const integrationPills = [ { name: 'Signal', icon: siIcon(siSignal), color: '#3A76F0' }, { name: 'iMessage', icon: siIcon(siApple), color: '#007AFF' }, { name: 'Claude', icon: siIcon(siAnthropic), color: '#D4A574' }, - { name: 'GPT', icon: 'lucide:bot', color: '#00A67E' }, - { name: 'Spotify', icon: siIcon(siSpotify), color: '#1DB954' }, - { name: 'Hue', icon: siIcon(siPhilipshue), color: '#0065D3' }, - { name: 'Obsidian', icon: siIcon(siObsidian), color: '#7C3AED' }, - { name: 'Twitter', icon: siIcon(siX), color: '#FFFFFF' }, + { name: 'ChatGPT', icon: 'lucide:bot', color: '#10a37f' }, { name: 'Browser', icon: siIcon(siGooglechrome), color: '#4285F4' }, { name: 'Gmail', icon: siIcon(siGmail), color: '#EA4335' }, - { name: 'GitHub', icon: siIcon(siGithub), color: '#FFFFFF' }, + { name: 'GitHub', icon: siIcon(siGithub), color: '#24292e' }, + { name: 'Twitter', icon: siIcon(siX), color: '#000000' }, ]; // Split top 30 into two rows for carousel @@ -51,6 +50,8 @@ const duration2 = (row2.length / 2 * pixelsPerItem) / pixelsPerSecond; --- + +
@@ -58,28 +59,7 @@ const duration2 = (row2.length / 2 * pixelsPerItem) / pixelsPerSecond;

@@ -90,7 +70,13 @@ const duration2 = (row2.length / 2 * pixelsPerItem) / pixelsPerSecond;

Clears your inbox, sends emails, manages your calendar, checks you in for flights.
- All from WhatsApp, Telegram, or any chat app you already use. + All from + + + WhatsApp, + + + Telegram, or any chat app you already use.

@@ -105,56 +91,8 @@ const duration2 = (row2.length / 2 * pixelsPerItem) / pixelsPerSecond; )} - -
-
-

- What People Say -

- View all community shoutouts - - -
-
-
- {row1.map((t) => ( - - {t.author} -
-

"{t.quote}"

- @{t.author} -
-
- ))} -
-
- {row2.map((t) => ( - - {t.author} -
-

"{t.quote}"

- @{t.author} -
-
- ))} -
-
-
- -
+

Quick Start

@@ -581,6 +519,41 @@ const duration2 = (row2.length / 2 * pixelsPerItem) / pixelsPerSecond; }); }); + // Navbar Scroll Effect + const navbar = document.getElementById('navbar'); + let lastScrollY = window.scrollY; + + window.addEventListener('scroll', () => { + const currentScrollY = window.scrollY; + + if (currentScrollY > 20) { + navbar.classList.add('scrolled'); + } else { + navbar.classList.remove('scrolled'); + } + + lastScrollY = currentScrollY; + }); + + // Smooth scroll for navbar links + document.querySelectorAll('.navbar-link').forEach(link => { + link.addEventListener('click', (e) => { + const href = link.getAttribute('href'); + if (href.startsWith('#')) { + e.preventDefault(); + const target = document.querySelector(href); + if (target) { + const offset = 80; // Account for navbar height + const targetPosition = target.offsetTop - offset; + window.scrollTo({ + top: targetPosition, + behavior: 'smooth' + }); + } + } + }); + }); + // Easter egg: Dalek mode on lobster hover (null-safe) const lobsterIcon = document.querySelector('.lobster-icon'); const tagline = document.getElementById('tagline'); @@ -617,8 +590,56 @@ const duration2 = (row2.length / 2 * pixelsPerItem) / pixelsPerSecond; } + +
+
+

+ What People Say +

+ View all community shoutouts + + +
+
+
+ {row1.map((t) => ( + + {t.author} +
+

"{t.quote}"

+ @{t.author} +
+
+ ))} +
+
+ {row2.map((t) => ( + + {t.author} +
+

"{t.quote}"

+ @{t.author} +
+
+ ))} +
+
+
+ -
+

What It Does

@@ -661,21 +682,83 @@ const duration2 = (row2.length / 2 * pixelsPerItem) / pixelsPerSecond;

Works With Everything

-
- {integrationPills.map((p) => ( - - - {p.name} - - ))} -
-
+ + +

@@ -684,9 +767,7 @@ const duration2 = (row2.length / 2 * pixelsPerItem) / pixelsPerSecond;

- - -