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
1 change: 1 addition & 0 deletions .bun-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1.3.14
8 changes: 4 additions & 4 deletions apps/guides/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default function Home() {

return (
<div>
{/* Hero Section */}
{/* Hero Section — server-rendered for fast LCP */}
<section className="relative py-24 overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-b from-blue-50 to-white dark:from-blue-950/20 dark:to-background" />
<div className="container relative text-center">
Expand All @@ -38,7 +38,7 @@ export default function Home() {
</div>
</section>

{/* Features Section */}
{/* Features Section — server-rendered */}
<section className="py-20">
<div className="container">
<div className="grid gap-10 md:grid-cols-3">
Expand All @@ -55,7 +55,7 @@ export default function Home() {
</div>
</section>

{/* Featured Guides */}
{/* Featured Guides — server-rendered */}
<section className="py-20 bg-apple-gray-light dark:bg-gray-900/20">
<div className="container">
<h2 className="mb-10 text-3xl font-semibold tracking-tight text-center">
Expand All @@ -65,7 +65,7 @@ export default function Home() {
</div>
</section>

{/* Filterable guides grid (client component handles search params) */}
{/* Filterable guides grid client component for search/filter UI only */}
<FilterableGuides allPosts={allPosts} categories={categories} />
</div>
);
Expand Down
9 changes: 1 addition & 8 deletions apps/guides/components/footer.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
'use client';

import { useQuery } from '@tanstack/react-query';
import { assertDefined } from 'guides-app/lib/assertDefined';
import { getAllCategories } from 'guides-app/lib/categories';
import { footerConfig, siteConfig } from 'guides-app/lib/config';
import { Backpack, Globe } from 'lucide-react';
import Link from 'next/link';

export default function Footer() {
// Fetch categories using TanStack Query
const { data: categories = [] } = useQuery({
queryKey: ['categories'],
queryFn: getAllCategories,
});
const categories = getAllCategories();

const company = footerConfig.mainSections[1];
assertDefined(company);
Expand Down
8 changes: 1 addition & 7 deletions apps/guides/components/providers/query-provider.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use client';

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { type ReactNode, useState } from 'react';

export function QueryProvider({ children }: { children: ReactNode }) {
Expand All @@ -17,10 +16,5 @@ export function QueryProvider({ children }: { children: ReactNode }) {
}),
);

return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
}
4 changes: 4 additions & 0 deletions apps/guides/styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ body {
}

@layer base {
html {
scroll-behavior: smooth;
}

Comment on lines +17 to +19
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting scroll-behavior: smooth unconditionally can cause motion/vestibular issues for users who prefer reduced motion. Consider applying smooth scrolling only under @media (prefers-reduced-motion: no-preference) (and falling back to auto otherwise).

Suggested change
scroll-behavior: smooth;
}
scroll-behavior: auto;
}
@media (prefers-reduced-motion: no-preference) {
html {
scroll-behavior: smooth;
}
}

Copilot uses AI. Check for mistakes.
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
Expand Down
28 changes: 11 additions & 17 deletions apps/landing/components/app-preview.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
'use client';

import { AnimatePresence, motion } from 'framer-motion';
import { assertDefined } from 'landing-app/lib/typeAssertions';
import Image from 'next/image';
import { useEffect, useState } from 'react';

Expand Down Expand Up @@ -29,28 +27,24 @@ export default function AppPreview() {
return () => clearInterval(interval);
}, [screens.length]);

assertDefined(screens[currentScreen]);

return (
<>
<AnimatePresence mode="wait">
<motion.div
key={currentScreen}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.5 }}
className="absolute inset-0"
{screens.map((screen, index) => (
<div
key={screen.src}
className={`absolute inset-0 transition-opacity duration-500 ${
index === currentScreen ? 'opacity-100' : 'opacity-0'
}`}
>
<Image
src={screens[currentScreen].src || '/placeholder.svg'}
alt={screens[currentScreen].alt}
src={screen.src}
alt={screen.alt}
fill
className="object-cover"
priority
priority={index === 0}
/>
Comment on lines +32 to 45
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change keeps all preview screens (and their next/image instances) mounted at once and only toggles opacity. Because all images are in-viewport, Next will typically start fetching each non-priority image as well, which can increase network contention and hurt LCP. Consider rendering only the active screen (or at most active + next for crossfade) so off-screen frames don't trigger image downloads until needed.

Copilot uses AI. Check for mistakes.
</motion.div>
</AnimatePresence>
</div>
))}
<div className="absolute bottom-5 left-0 right-0 flex justify-center gap-2 z-10">
{screens.map((_, index) => (
<button
Expand Down
117 changes: 34 additions & 83 deletions apps/landing/components/sections/landing-hero.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,9 @@
'use client';

import { motion } from 'framer-motion';
import DeviceMockup from 'landing-app/components/ui/device-mockup';
import { siteConfig } from 'landing-app/config/site';
import { ArrowRight, Download, Star } from 'lucide-react';
import Link from 'next/link';
import type React from 'react';

export default function LandingHero() {
const scrollToSection = (e: React.MouseEvent<HTMLAnchorElement>, href: string) => {
e.preventDefault();
const targetId = href.substring(1);
const element = document.getElementById(targetId);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
};

const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { staggerChildren: 0.1, delayChildren: 0.2 },
},
};

const itemVariants = {
hidden: { y: 20, opacity: 0 },
visible: {
y: 0,
opacity: 1,
transition: { type: 'spring' as const, stiffness: 100, damping: 10 },
},
};

return (
<section className="relative py-24 overflow-hidden">
{/* Subtle gradient background – Apple style */}
Expand All @@ -42,47 +12,35 @@ export default function LandingHero() {
<div className="container">
<div className="grid gap-12 lg:grid-cols-2 lg:gap-16 items-center">
{/* Text column */}
<motion.div
className="space-y-6 max-w-2xl mx-auto lg:mx-0 text-center lg:text-left"
variants={containerVariants}
initial="hidden"
animate="visible"
>
<div className="space-y-6 max-w-2xl mx-auto lg:mx-0 text-center lg:text-left">
{/* Badge */}
<motion.div variants={itemVariants}>
<div className="animate-fade-up" style={{ animationDelay: '0.2s' }}>
<div className="apple-badge mx-auto lg:mx-0 w-fit">
<span className="mr-1.5 h-2 w-2 rounded-full bg-apple-blue animate-pulse inline-block" />
{siteConfig.hero.badge}
</div>
</motion.div>
</div>

{/* Heading */}
<motion.h1
className="text-4xl font-semibold tracking-tight sm:text-5xl md:text-6xl"
variants={itemVariants}
>
{/* Heading — rendered immediately for LCP */}
<h1 className="text-4xl font-semibold tracking-tight sm:text-5xl md:text-6xl">
<span className="block text-foreground">{siteConfig.hero.titleLine1}</span>
<span className="block mt-1 bg-clip-text text-transparent bg-gradient-to-r from-apple-blue to-blue-400">
{siteConfig.hero.titleLine2}
</span>
</motion.h1>
</h1>

{/* Subtitle */}
<motion.p
className="text-xl text-muted-foreground font-medium max-w-xl mx-auto lg:mx-0"
variants={itemVariants}
>
{/* Subtitle — rendered immediately for LCP */}
<p className="text-xl text-muted-foreground font-medium max-w-xl mx-auto lg:mx-0">
{siteConfig.hero.subtitle}
</motion.p>
</p>

{/* CTA buttons */}
<motion.div
className="flex flex-col sm:flex-row justify-center lg:justify-start gap-3"
variants={itemVariants}
<div
className="flex flex-col sm:flex-row justify-center lg:justify-start gap-3 animate-fade-up"
style={{ animationDelay: '0.3s' }}
>
<Link
href={siteConfig.cta.primary.href}
onClick={(e) => scrollToSection(e, siteConfig.cta.primary.href)}
className="group inline-flex items-center justify-center gap-2 rounded-full bg-apple-blue text-white px-8 h-12 text-sm font-medium hover:bg-apple-blue/90 transition-colors"
>
<Download className="h-4 w-4" />
Expand All @@ -92,28 +50,27 @@ export default function LandingHero() {

<Link
href={siteConfig.cta.secondary.href}
onClick={(e) => scrollToSection(e, siteConfig.cta.secondary.href)}
className="inline-flex items-center justify-center gap-2 rounded-full border border-border bg-background px-8 h-12 text-sm font-medium hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
>
{siteConfig.cta.secondary.text}
</Link>
</motion.div>
</div>

{/* Social proof */}
{siteConfig.hero.socialProof && (
<motion.p
className="text-xs text-muted-foreground flex items-center justify-center lg:justify-start gap-1"
variants={itemVariants}
<p
className="text-xs text-muted-foreground flex items-center justify-center lg:justify-start gap-1 animate-fade-up"
style={{ animationDelay: '0.4s' }}
>
<Star className="w-3 h-3 text-amber-400 fill-amber-400 flex-shrink-0" />
{siteConfig.hero.socialProof}
</motion.p>
</p>
)}

{/* Stats */}
<motion.div
className="flex flex-col sm:flex-row items-center justify-center lg:justify-start gap-6 pt-4"
variants={itemVariants}
<div
className="flex flex-col sm:flex-row items-center justify-center lg:justify-start gap-6 pt-4 animate-fade-up"
style={{ animationDelay: '0.5s' }}
>
<div className="flex -space-x-2">
{siteConfig.testimonials.items.slice(0, 4).map((user) => (
Expand All @@ -134,15 +91,13 @@ export default function LandingHero() {
</div>
))}
</div>
</motion.div>
</motion.div>
</div>
</div>

{/* Device mockup column */}
<motion.div
className="relative mx-auto lg:mx-0"
initial={{ scale: 0.95 }}
animate={{ scale: 1 }}
transition={{ duration: 0.5, delay: 0.2 }}
<div
className="relative mx-auto lg:mx-0 animate-scale-in"
style={{ animationDelay: '0.2s' }}
>
<DeviceMockup
image="/images/app/dashboard.png"
Expand All @@ -153,11 +108,9 @@ export default function LandingHero() {
/>

{/* Floating cards – Apple style: clean card with subtle shadow */}
<motion.div
className="absolute top-[10%] -left-16 hidden lg:block"
initial={{ x: -20 }}
animate={{ x: 0 }}
transition={{ duration: 0.5, delay: 0.6 }}
<div
className="absolute top-[10%] -left-16 hidden lg:block animate-slide-in-left"
style={{ animationDelay: '0.6s' }}
>
<div className="apple-card p-3">
<div className="flex items-center gap-2">
Expand All @@ -174,13 +127,11 @@ export default function LandingHero() {
</div>
</div>
</div>
</motion.div>
</div>

<motion.div
className="absolute bottom-[15%] -right-10 hidden lg:block"
initial={{ x: 20 }}
animate={{ x: 0 }}
transition={{ duration: 0.5, delay: 0.8 }}
<div
className="absolute bottom-[15%] -right-10 hidden lg:block animate-slide-in-right"
style={{ animationDelay: '0.8s' }}
>
<div className="apple-card p-3">
<div className="flex items-center gap-2">
Expand All @@ -193,8 +144,8 @@ export default function LandingHero() {
</div>
</div>
</div>
</motion.div>
</motion.div>
</div>
</div>
</div>
</div>
</section>
Expand Down
15 changes: 0 additions & 15 deletions apps/landing/components/site-footer.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,9 @@
'use client';

import { siteConfig } from 'landing-app/config/site';
import { LucideIcon, TikTokIcon } from 'landing-app/lib/icons';
import { Backpack } from 'lucide-react';
import Link from 'next/link';
import type React from 'react';

export default function SiteFooter() {
const scrollToSection = (e: React.MouseEvent<HTMLAnchorElement>, href: string) => {
e.preventDefault();
const targetId = href.substring(1);
const element = document.getElementById(targetId);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
};

return (
<footer className="border-t border-border/40 py-10 md:py-14 lg:py-16 bg-apple-gray-light dark:bg-gray-900/20">
<div className="container">
Expand Down Expand Up @@ -58,7 +46,6 @@ export default function SiteFooter() {
<li key={item.title}>
<Link
href={item.href}
onClick={(e) => item.href.startsWith('#') && scrollToSection(e, item.href)}
className="text-sm text-muted-foreground hover:text-apple-blue transition-colors"
>
{item.title}
Expand All @@ -76,7 +63,6 @@ export default function SiteFooter() {
<li key={item.title}>
<Link
href={item.href}
onClick={(e) => item.href.startsWith('#') && scrollToSection(e, item.href)}
className="text-sm text-muted-foreground hover:text-apple-blue transition-colors"
>
{item.title}
Expand All @@ -94,7 +80,6 @@ export default function SiteFooter() {
<li key={item.title}>
<Link
href={item.href}
onClick={(e) => item.href.startsWith('#') && scrollToSection(e, item.href)}
className="text-sm text-muted-foreground hover:text-apple-blue transition-colors"
>
{item.title}
Expand Down
Loading
Loading