-
+
{parts.map((part, i) =>
highlights.includes(part) ? (
@@ -69,230 +73,13 @@ function ArchDiagram() {
);
}
-function QueryCard() {
- const [selectedIdx, setSelectedIdx] = useState(0);
- const [activeTab, setActiveTab] = useState<'query' | 'result' | 'sql'>(
- 'query',
- );
- const example = queryExamples[selectedIdx];
-
- return (
-
-
- {queryExamples.map((ex, i) => (
-
- ))}
-
-
-
- {(
- [
- ['query', 'InstaQL'],
- ['result', 'Result'],
- ['sql', 'Equivalent SQL'],
- ] as const
- ).map(([key, label]) => (
-
- ))}
-
-
-
-
- );
-}
-
-function TransactionCard() {
- const [selectedIdx, setSelectedIdx] = useState(0);
- const example = transactionExamples[selectedIdx];
-
- return (
-
-
- {transactionExamples.map((ex, i) => (
-
- ))}
-
-
-
- );
-}
-
-function TypeSafetySection() {
- const [selectedIdx, setSelectedIdx] = useState(0);
- const block = typeSafetyBlocks[selectedIdx];
-
- return (
-
-
-
Full type-safety
-
- Instant provides full type-safety for your schema, queries, and
- transactions. This means you can catch errors at compile time instead
- of runtime. If you change your schema your types will automagically
- update too!
-
-
- This also helps with LLMs since they can use the types to better
- understand your data and generate valid queries and transactions for
- you. If they make a mistake they can get immediate feedback from the
- type system.
-
-
-
-
-
- {typeSafetyBlocks.map((b, i) => (
-
- ))}
-
-
-
-
-
- );
-}
-
-function FeatureSection({
- card,
- details,
- reverse,
-}: {
- card: React.ReactNode;
- details: React.ReactNode;
- reverse?: boolean;
-}) {
- return (
-
-
- {details}
-
-
- {card}
-
-
- );
-}
-
-function CLIDetails() {
- return (
- <>
-
- CLI tools for agentic development
-
-
- Our CLI tools make it easy to manage your Instant project and scaffold
- new ones. Anything you can do in the dashboard your agent can do from
- the terminal.
-
- >
- );
-}
-
export default function Database() {
const title = 'Database - Instant';
const description =
'Instant has everything you need to build web and mobile apps with your favorite LLM.';
return (
-
+
{title}
@@ -309,225 +96,237 @@ export default function Database() {
/>
-
-
-
-
+
- {/* Hero */}
-
-
-
-
-
- Instant Database
-
-
- The best database for
- AI-coded apps.
-
-
- {description}
-
-
-
-
-
-
-
-
+ {/* Hero */}
+
+
+
+
+
+
+ The best database for
+ AI-coded apps.
+
+
{description}
+
+ Get started
+
+ Read the docs
+
+
+
+
- {/* Features */}
-
-
-
- {/* Easy to query */}
-
}
- details={
- <>
-
- Easy to query
-
-
- Instant's query language, InstaQL, is designed to be
- easy to understand. It uses plain JavaScript objects to
- declare the data you want to fetch and the shape you
- want.
-
-
- If you ever need to change what to fetch you can just
- change the query object and your UI will update on its
- own. No need for a build step or to update any backend
- code.
-
-
- InstaQL supports filtering, sorting, pagination and
- more. The syntax is simple enough to learn in minutes
- and LLMs can easily generate queries for you too!
-
- >
- }
- />
+ {/* Features */}
+
+
+ {/* Easy to query */}
+
+
+
Easy to query
+
+ Instant's query language, InstaQL, is designed to be easy to
+ understand. It uses plain JavaScript objects to declare the data
+ you want to fetch and the shape you want.
+
+
+ If you ever need to change what to fetch you can just change the
+ query object and your UI will update on its own. No need for a
+ build step or to update any backend code.
+
+
+ InstaQL supports filtering, sorting, pagination and more. The
+ syntax is simple enough to learn in minutes and LLMs can easily
+ generate queries for you too!
+
+
+
+
+
+
- {/* Easy to transact */}
-
}
- details={
- <>
-
- Easy to transact
-
-
- Modifying data in Instant is easy with our transaction
- language InstaML.
-
-
- Making changes is as simple as calling{' '}
-
- create
-
- ,{' '}
-
- update
-
- , or{' '}
-
- delete
-
- . You can also use{' '}
-
- link
- {' '}
- and{' '}
-
- unlink
- {' '}
- to manage relationships between entities.
-
-
- Transactions are atomically committed with{' '}
-
- db.transact
- {' '}
- and you're guaranteed to end up with consistent data.
-
- >
- }
+ {/* Easy to transact */}
+
+
+
+
+
+
+
Easy to transact
+
+ Modifying data in Instant is easy with our transaction
+ language InstaML.
+
+
+ Making changes is as simple as calling{' '}
+
+ create
+
+ ,{' '}
+
+ update
+
+ , or{' '}
+
+ delete
+
+ . You can also use{' '}
+
+ link
+ {' '}
+ and{' '}
+
+ unlink
+ {' '}
+ to manage relationships between entities.
+
+
+ Transactions are atomically committed with{' '}
+
+ db.transact
+ {' '}
+ and you're guaranteed to end up with consistent data.
+
+
+
+
- {/* No need for a server */}
-
-
-
- No need for a server
-
-
- Use our client SDKs to use InstantDB directly from your
- frontend. No need to manage a database or server.
-
-
- You can just focus on building an amazing app and we'll
- handle the rest.
-
-
-
-
+ {/* No need for a server */}
+
+
+
+
No need for a server
+
+ Use our client SDKs to use InstantDB directly from your
+ frontend. No need to manage a database or server.
+
+
+ You can just focus on building an amazing app and we'll handle
+ the rest.
+
+
+
+
+
- {/* Full type-safety */}
-
+ {/* Full type-safety */}
+
+
+
+
+
+
+
Full type-safety
+
+ Instant provides full type-safety for your schema, queries,
+ and transactions. This means you can catch errors at compile
+ time instead of runtime. If you change your schema your types
+ will automagically update too!
+
+
+ This also helps with LLMs since they can use the types to
+ better understand your data and generate valid queries and
+ transactions for you. If they make a mistake they can get
+ immediate feedback from the type system.
+
+
+
+
- {/* CLI Tools */}
-
}
- details={
-
-
-
- }
- />
+ {/* CLI Tools */}
+
+
+
+
CLI tools for agentic development
+
+ Our CLI tools make it easy to manage your Instant project and
+ scaffold new ones. Anything you can do in the dashboard your
+ agent can do from the terminal.
+
-
-
+
+
+
+
+
- {/* Generous free tier */}
-
-
-
-
- The most generous
-
- free tier in databases.
-
-
-
-
-
Unlimited
-
- Free projects
-
-
- Create as many apps as you want. We never pause inactive
- projects.
-
-
-
-
Free
-
- To get started
-
-
- No credit card. No trial period. No restrictions for
- commercial use.
-
-
-
-
Scales
-
- When you need it
-
-
- When you're ready to grow, we have plans that scale with
- your usage.
-
-
+ {/* Generous free tier */}
+
+
+
+
+
+
+
+ The most generous
+ free tier in databases.
+
+
+
+
Unlimited
+
Free projects
+
+ Create as many apps as you want. We never pause inactive
+ projects.
+
-
-
-
+
+
Free
+
To get started
+
+ No credit card. No trial period. No restrictions for
+ commercial use.
+
+
+
+
Scales
+
When you need it
+
+ When you're ready to grow, we have plans that scale with
+ your usage.
+
-
- Instant is{' '}
-
- 100% Open Source
-
-
-
-
-
-
+
+ Start building
+
+ View pricing
+
+
+
+ Instant is{' '}
+
+ 100% Open Source
+
+
+
+
+
-
+
+
+
);
}
diff --git a/client/www/pages/product/storage/index.tsx b/client/www/pages/product/storage/index.tsx
index 577d99f98c..ceaa009255 100644
--- a/client/www/pages/product/storage/index.tsx
+++ b/client/www/pages/product/storage/index.tsx
@@ -1,101 +1,275 @@
-import { useState } from 'react';
+import { useState, useRef, useEffect } from 'react';
+import { PlayIcon, PauseIcon } from '@heroicons/react/24/solid';
import { AnimatePresence, motion } from 'motion/react';
import Head from 'next/head';
import * as og from '@/lib/og';
-import {
- LandingContainer,
- LandingFooter,
- MainNav,
- ProductNav,
- SectionWide,
-} from '@/components/marketingUi';
-import { Button, Fence, cn } from '@/components/ui';
+import { MainNav, ProductNav } from '@/components/marketingUi';
+import { cn } from '@/components/ui';
import {
storageExamples,
permissionExamples,
} from '@/lib/product/storage/examples';
+import { Section } from '@/components/new-landing/Section';
+import {
+ LandingButton,
+ SectionTitle,
+ SectionSubtitle,
+ Subheading,
+} from '@/components/new-landing/typography';
+import { Footer } from '@/components/new-landing/Footer';
+import { TopWash } from '@/components/new-landing/TopWash';
+import { AnimateIn } from '@/components/new-landing/AnimateIn';
+import { TabbedCodeExample } from '@/components/new-landing/TabbedCodeExample';
+import { PreviewPlayer, tracks } from '@/lib/product/storage/musicPreview';
+import exampleDB from '@/lib/intern/docs-feedback/db';
+
+function FrequencyBars({ player }: { player: PreviewPlayer }) {
+ const barsRef = useRef<(HTMLDivElement | null)[]>([]);
+ const rafRef = useRef(0);
+
+ useEffect(() => {
+ const update = () => {
+ const [low, mid, high] = player.getFrequencyBars();
+ const values = [low, mid, high];
+ barsRef.current.forEach((bar, i) => {
+ if (bar) {
+ const h = 3 + values[i] * 13;
+ bar.style.height = `${h}px`;
+ }
+ });
+ rafRef.current = requestAnimationFrame(update);
+ };
+ rafRef.current = requestAnimationFrame(update);
+ return () => cancelAnimationFrame(rafRef.current);
+ }, [player]);
+
+ return (
+
+ {[0, 1, 2].map((j) => (
+
{
+ barsRef.current[j] = el;
+ }}
+ className="w-[2.5px] rounded-sm bg-gray-900 transition-[height] duration-75"
+ style={{ height: '3px' }}
+ />
+ ))}
+
+ );
+}
function MusicApp() {
- const tracks = [
- { title: 'Midnight City', artist: 'M83', duration: '4:03' },
- { title: 'Intro', artist: 'The xx', duration: '2:07' },
- { title: 'Tadow', artist: 'Masego & FKJ', duration: '5:48' },
- { title: 'Rhiannon', artist: 'Fleetwood Mac', duration: '4:13' },
- ];
+ const [activeTrack, setActiveTrack] = useState(0);
+ const [playing, setPlaying] = useState(false);
+ const playerRef = useRef
(null);
+ const activeTrackRef = useRef(activeTrack);
+ activeTrackRef.current = activeTrack;
+
+ const getPlayer = () => {
+ if (!playerRef.current) {
+ const player = new PreviewPlayer();
+ player.onTrackEnd = () => {
+ const next = (activeTrackRef.current + 1) % tracks.length;
+ setActiveTrack(next);
+ player.play(next);
+ };
+ playerRef.current = player;
+ }
+ return playerRef.current;
+ };
+
+ useEffect(() => {
+ return () => {
+ playerRef.current?.stop();
+ };
+ }, []);
+
return (
-
-
-
-
Now Playing
-
Midnight City - M83
-
-
-
+
+
+
+
+ Favorite classical
+
+
+

+
Stopa
+
-
- {tracks.map((t, i) => (
-
-
{i + 1}
-
-
-
{t.title}
-
{t.artist}
+
+ {tracks.map((t, i) => {
+ const isActive = i === activeTrack;
+ return (
+
{
+ const player = getPlayer();
+ setActiveTrack(i);
+ setPlaying(true);
+ player.play(i);
+ }}
+ className="flex cursor-pointer items-center gap-3 px-4 py-2.5 hover:bg-gray-50"
+ >
+
+ {isActive && playing && playerRef.current ? (
+
+ ) : (
+ {i + 1}
+ )}
+
+
-
{t.duration}
-
- ))}
+ );
+ })}
);
}
-const logos = [
- { src: '/img/icon/logo-512.svg', alt: 'Instant', bg: 'bg-orange-500' },
- {
- src: '/img/product-pages/sync/figma.svg',
- alt: 'Figma',
- bg: 'bg-purple-100',
- },
- {
- src: '/img/product-pages/sync/notion.svg',
- alt: 'Notion',
- bg: 'bg-orange-50',
- },
- {
- src: '/img/product-pages/storage/linear-white.svg',
- alt: 'Linear',
- bg: 'bg-gray-950',
- },
-];
+function animateHeart(target: HTMLElement) {
+ const count = 3 + Math.floor(Math.random() * 3);
+ for (let i = 0; i < count; i++) {
+ const el = document.createElement('div');
+ el.innerText = '❤️';
+ target.appendChild(el);
+
+ const size = 14 + Math.random() * 14;
+ const xDrift = (Math.random() - 0.5) * 60;
+ const yDist = -(50 + Math.random() * 40);
+ const delay = i * 60;
+ const duration = 600 + Math.random() * 300;
+ const rotation = (Math.random() - 0.5) * 40;
+
+ Object.assign(el.style, {
+ position: 'absolute',
+ left: '50%',
+ top: '50%',
+ fontSize: `${size}px`,
+ lineHeight: '1',
+ pointerEvents: 'none',
+ zIndex: '9999',
+ transform: 'translate(-50%, -50%) scale(0)',
+ opacity: '1',
+ transition: `transform ${duration}ms cubic-bezier(0.2, 0.6, 0.3, 1), opacity ${duration}ms ease-out`,
+ transitionDelay: `${delay}ms`,
+ });
+
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ Object.assign(el.style, {
+ transform: `translate(calc(-50% + ${xDrift}px), calc(-50% + ${yDist}px)) scale(1) rotate(${rotation}deg)`,
+ opacity: '0',
+ });
+ });
+ });
+
+ setTimeout(() => el.remove(), duration + delay + 50);
+ }
+}
+
+const storageRoom = exampleDB.room('homepageStorageDemo', 'storage');
function PhotoApp() {
+ const heartRef = useRef
(null);
+
+ const publishHeart = exampleDB.rooms.usePublishTopic(storageRoom, 'hearts');
+
+ exampleDB.rooms.useTopicEffect(storageRoom, 'hearts', () => {
+ if (heartRef.current) animateHeart(heartRef.current);
+ });
+
+ const handleHeartClick = () => {
+ publishHeart({});
+ if (heartRef.current) animateHeart(heartRef.current);
+ };
+
return (
-
-
+
+ {/* Header */}
+

-
instant
+
stopa
-
- {logos.map((logo) => (
-
+

+ {/* Heart button */}
+
+
- ))}
+ ❤️
+
+
+
+ {/* Caption */}
+
+
+ stopa{' '}
+ Newest member of the team
+
);
@@ -103,40 +277,55 @@ function PhotoApp() {
const books = [
{
- title: 'How to Win Friends',
+ title: 'How to Win Friends and Influence People',
author: 'Dale Carnegie',
cover: '/img/product-pages/storage/book-1.webp',
+ description:
+ "Dale Carnegie's rock-solid, time-tested advice has carried countless people up the ladder of success in their business and personal lives.",
},
{
- title: '7 Habits',
- author: 'Stephen Covey',
+ title: 'The 7 Habits of Highly Effective People',
+ author: 'Stephen R. Covey',
cover: '/img/product-pages/storage/book-5.webp',
+ description:
+ 'A leading management consultant outlines seven organizational rules for improving effectiveness and increasing productivity at work and at home.',
},
{
title: 'East of Eden',
author: 'John Steinbeck',
cover: '/img/product-pages/storage/book-3.webp',
+ description:
+ "A masterpiece of Biblical scope, and the magnum opus of one of America's most enduring authors. Set in the rich farmland of California's Salinas Valley.",
},
{
title: 'Antifragile',
- author: 'Nassim Taleb',
+ author: 'Nassim Nicholas Taleb',
cover: '/img/product-pages/storage/book-4.webp',
+ description:
+ 'Shares insights into how adversity can bring out the best in individuals and communities, drawing on multiple disciplines.',
},
{
- title: 'SICP',
- author: 'Abelson & Sussman',
+ title: 'Structure and Interpretation of Computer Programs',
+ author: 'Harold Abelson & Gerald Jay Sussman',
cover: '/img/product-pages/storage/book-2.webp',
+ description:
+ 'The foundational computer science textbook, licensed under Creative Commons. A deep dive into the simplicity behind our craft.',
},
{
title: 'Hackers & Painters',
author: 'Paul Graham',
cover: '/img/product-pages/storage/book-6.webp',
+ description:
+ 'Big ideas from the computer age. We are living in a world increasingly designed and engineered by computer programmers and software.',
},
];
function BookApp() {
+ const [selectedBook, setSelectedBook] = useState
(null);
+ const book = selectedBook !== null ? books[selectedBook] : null;
+
return (
-
+
Zeneca
-
- {books.map((b) => (
-
+
+ {books.map((b, i) => (
+

setSelectedBook(i)}
/>
-
{b.author}
))}
-
- );
-}
-
-function AppGallery() {
- return (
-
- );
-}
-
-function StorageCard() {
- const [selectedIdx, setSelectedIdx] = useState(0);
- const example = storageExamples[selectedIdx];
-
- return (
-
-
- {storageExamples.map((ex, i) => (
-
- ))}
-
-
-
-
+
+ {book && (
+ <>
setSelectedBook(null)}
+ />
+
-
+
+
+

+
+
-
-
-
+ >
+ )}
+
);
}
-function PermissionsCard() {
- const [selectedIdx, setSelectedIdx] = useState(0);
- const example = permissionExamples[selectedIdx];
+const appDemos = [
+ { label: 'Music', component: MusicApp },
+ { label: 'Books', component: BookApp },
+ { label: 'Photos', component: PhotoApp },
+];
+
+const cardStyles = [
+ 'rotate-[-2.5deg] translate-y-2 justify-self-start',
+ 'z-10 justify-self-center',
+ 'rotate-[1.5deg] translate-y-4 justify-self-end',
+];
+function AppGallery() {
return (
-
-
- {permissionExamples.map((ex, i) => (
-
);
}
@@ -273,7 +437,7 @@ export default function Storage() {
'Digital content is just another table in your database. No separate service needed.';
return (
-
+
{title}
@@ -290,143 +454,129 @@ export default function Storage() {
/>
-
-
-
-
-
- {/* Hero */}
-
-
-
-
-
- Instant Storage
-
-
- File storage and data
-
- in one place.
-
-
- {description}
-
-
-
-
-
-
-
-
+
+
+ {/* Hero */}
+
+
+
+
+
+
+ File storage and data
+ in one place.
+
+
{description}
+
+ Get started
+
+ Read the docs
+
+
+
+
+ {/* Features */}
+
+
{/* No need for a separate file storage system */}
-
-
-
-
-
- No need for a separate file storage system
-
-
- Instant comes with built-in file storage. No S3 buckets to
- configure, no signed URLs to manage.
-
-
- When you've got storage with your database you can easily
- build apps like Instagram, Spotify, Goodreads and more!
-
-
-
-
-
+
+
+
+ No need for a separate file storage system
+
+
+ Instant comes with built-in file storage. No S3 buckets to
+ configure, no signed URLs to manage.
+
+
+ When you've got storage with your database you can easily build
+ apps like Instagram, Spotify, Goodreads and more!
+
+
+
+
{/* Files are integrated into your database */}
-
-
-
-
-
- Files are integrated into your database
-
-
- Files are stored alongside other entities in Instant. Upload
- them, link them to your data, and query with InstaQL just
- like any other table.
-
-
- Best of all, your files are reactive too! When a file is
- updated or deleted, your UI updates in real-time.
-
-
-
-
-
+
+
+
+
Files are integrated into your database
+
+ Files are stored alongside other entities in Instant. Upload
+ them, link them to your data, and query with InstaQL just like
+ any other table.
+
+
+ Best of all, your files are reactive too! When a file is
+ updated or deleted, your UI updates in real-time.
+
+
+
+
{/* Secure with permissions */}
-
-
-
-
-
- Secure with permissions
-
-
- Files use the same permission system as the rest of your
- data. Control who can upload, view, and delete files with
- simple rules.
-
-
- Your rules can traverse relationships, check auth state, and
- enforce access at every level. No server endpoints needed.
-
-
-
+
+
+
+
Secure with permissions
+
+ Files use the same permission system as the rest of your data.
+ Control who can upload, view, and delete files with simple
+ rules.
+
+
+ Your rules can traverse relationships, check auth state, and
+ enforce access at every level. No server endpoints needed.
+
-
-
-
- {/* CTA */}
-
-
-
-
-
- Build rich applications
-
-
with files and data
- together.
-
-
-
-
-
+
+
-
-
+
+
-
+
+
+ {/* CTA */}
+
+
+
+
+
+
+
+ Build rich applications
+
with files and data together.
+
+
+ Get started
+
+ Read the docs
+
+
+
+
+
-
+
+
+
);
}
diff --git a/client/www/pages/product/sync/index.tsx b/client/www/pages/product/sync/index.tsx
index bf9d3e4949..66abfcd811 100644
--- a/client/www/pages/product/sync/index.tsx
+++ b/client/www/pages/product/sync/index.tsx
@@ -1,16 +1,22 @@
-import { useState } from 'react';
+import { ComponentType, useState } from 'react';
import Head from 'next/head';
import * as og from '@/lib/og';
import Image from 'next/image';
+import { MainNav, ProductNav } from '@/components/marketingUi';
+import { Section } from '@/components/new-landing/Section';
import {
- LandingContainer,
- LandingFooter,
- MainNav,
- ProductNav,
- SectionWide,
-} from '@/components/marketingUi';
-import { Button } from '@/components/ui';
-import { features, layers, hardClosing } from '@/lib/product/sync/examples';
+ LandingButton,
+ SectionTitle,
+ SectionSubtitle,
+ Subheading,
+} from '@/components/new-landing/typography';
+import { Footer } from '@/components/new-landing/Footer';
+import { TopWash } from '@/components/new-landing/TopWash';
+import { AnimateIn } from '@/components/new-landing/AnimateIn';
+import { RealtimeSyncWalkthrough } from '@/components/product/sync/RealtimeSyncWalkthrough';
+import { OptimisticUpdateDiagram } from '@/components/product/sync/OptimisticUpdateDiagram';
+import { ConflictResolutionWalkthrough } from '@/components/product/sync/ConflictResolutionWalkthrough';
+import { OfflinePersistenceWalkthrough } from '@/components/product/sync/OfflinePersistenceWalkthrough';
import figmaIcon from '@/public/img/product-pages/sync/figma.svg';
import notionIcon from '@/public/img/product-pages/sync/notion.svg';
@@ -22,45 +28,72 @@ const syncCompanies = [
{ name: 'Linear', icon: linearIcon },
];
-function DiagramPre({
- diagram,
- highlights,
-}: {
- diagram: string;
- highlights: string[];
-}) {
- const escaped = highlights.map((h) =>
- h.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'),
- );
- const pattern = new RegExp(`(${escaped.join('|')})`, 'g');
- const parts = diagram.split(pattern);
+const features = [
+ {
+ title: 'Every interaction is instant',
+ description:
+ 'There is no loading spinner, lag, or waiting. When you click a button, the app responds immediately.',
+ },
+ {
+ title: 'Collaboration is enabled by default',
+ description:
+ "No need to pull to refresh. You can work together in real-time and see each other's changes instantly.",
+ },
+ {
+ title: "Apps keep working even when you're offline",
+ description:
+ "You can keep using the app and your changes will sync when you come back online. Imagine using your favorite note-taking app and it doesn't load when your connection is spotty. That's not delightful.",
+ },
+];
- return (
-
-
- {parts.map((part, i) =>
- highlights.includes(part) ? (
-
- {part}
-
- ) : (
- part
- ),
- )}
-
-
- );
-}
+const layers: {
+ title: string;
+ why: string;
+ description: string;
+ Walkthrough: ComponentType;
+}[] = [
+ {
+ title: 'Optimistic Update Layer',
+ why: 'Users want instant feedback. Without optimistic updates, every action waits for a server round trip.',
+ description:
+ "When a user makes a change we first apply it to a local store so users see the update immediately. We'll also need to track this as a pending mutation. That way we can rollback if the server rejects the mutation. If the server accepts, we clear the mutation from the pending queue.",
+ Walkthrough: OptimisticUpdateDiagram,
+ },
+ {
+ title: 'Real-time Sync',
+ why: "Users working together want to see each other's changes in real-time, not after a page refresh.",
+ description:
+ 'We need to do polling or websockets. Websockets will be more-real time but then we need to handle disconnects and reconnects. When changes come in we need to merge remote updates into our local store.',
+ Walkthrough: RealtimeSyncWalkthrough,
+ },
+ {
+ title: 'Offline Persistence',
+ why: "Users want to be able to use their apps even when offline. Spotty connections shouldn't mean lost work either.",
+ description:
+ "We need to persist queries and mutations to IndexedDB in case the user goes offline. When the user comes back, we replay their queued transactions in order. Any transactions that have already been acknowledged are removed so the store doesn't grow forever.",
+ Walkthrough: OfflinePersistenceWalkthrough,
+ },
+ {
+ title: 'Conflict Resolution',
+ why: 'When you allow collaboration, you need to handle what happens when two people edit the same thing at once.',
+ description:
+ 'Alyssa and Louis both edit the same shape at the same time. Who wins? We need a strategy to decide (for example last write wins). We also need to rollback clients who have inconsistent optimistic state.',
+ Walkthrough: ConflictResolutionWalkthrough,
+ },
+];
+
+const hardClosing = [
+ 'This is a lot of code!',
+ "Doing it by hand will probably take too long. Even if AI writes it, you'll need to maintain it for every feature you build.",
+];
function HardSection() {
const [active, setActive] = useState(0);
const layer = layers[active];
return (
-
-
- Building these features is hard
-
-
+
+
Building these features is hard
+
Want to add these features to your app on your own? Here's what you'll
need to build.
@@ -78,11 +111,11 @@ function HardSection() {
}`}
>
{String(i + 1).padStart(2, '0')}
-
{l.title}
+
{l.title}
))}
@@ -94,8 +127,8 @@ function HardSection() {
{layer.why}
-
{layer.description}
-
+
{layer.description}
+
@@ -113,7 +146,7 @@ export default function SyncEngine() {
'Make every feature feel instant, be collaborative, and work offline. No extra code required.';
return (
-
+
{title}
@@ -130,133 +163,108 @@ export default function SyncEngine() {
/>
-
-
-
-
+
+
+ {/* Hero */}
+
+
+
+
+
+
+ Delightful applications
+
by default.
+
+
{description}
+
+ Get started
+
+ Read the docs
+
+
+
+
+
- {/* Hero */}
-
-
-
-
-
- Instant Sync
-
-
-
- Delightful applications
+ {/* Delightful apps share common features */}
+
+
+
+
+
Delightful apps share common features
+
+ It's easier than ever to build apps these days, especially when
+ you're using AI. However, making something delightful is still
+ hard. When you look at some of the best apps today, they all
+ have certain features in common.
+
+
+ {syncCompanies.map((company) => (
+
+
+
+ {company.name}
-
by default.
-
-
- Every feature you build will feel instant, be collaborative,
- and work offline.
- No extra code required.
-
-
-
-
-
+ ))}
-
-
-
- {/* Delightful apps share common features */}
-
-
-
-
-
-
- Delightful apps share common features
-
-
- It's easier than ever to build apps these days, especially
- when you're using AI. However, making something delightful
- is still hard. When you look at some of the best apps
- today, they all have certain features in common.
+
+
+ {features.map((f) => (
+
+
+
+
{f.title}
+
+ {f.description}
-
- {syncCompanies.map((company) => (
-
-
-
- {company.name}
-
-
- ))}
-
-
-
- {features.map((f) => (
-
-
-
-
{f.title}
-
{f.description}
-
-
- ))}
-
-
+ ))}
+
{/* Building these features is hard */}
-
-
-
-
-
+
+
+
+
+
- {/* Mini CTA: With Instant you get sync for free */}
-
-
-
-
- With Instant you get
- sync for free.
-
-
- In the past companies would hire a team of elite engineers to
- build a custom sync engine. In the future all apps will have
- sync by default.
-
-
-
-
-
+ {/* Mini CTA: With Instant you get sync for free */}
+
+
+
+
+
+
+
+ With Instant you get
+ sync for free.
+
+
+ In the past companies would hire a team of elite engineers to
+ build a custom sync engine. In the future all apps will have
+ sync by default.
+
+
+ Get started
+
+ Read the docs
+
-
-
-
-
+
+
+
-
+
+
+
);
}
diff --git a/client/www/pages/recipes.tsx b/client/www/pages/recipes.tsx
index 490b60bf5a..b483fdd81a 100644
--- a/client/www/pages/recipes.tsx
+++ b/client/www/pages/recipes.tsx
@@ -1,11 +1,11 @@
-import { Button, Copyable, Fence } from '@/components/ui';
+import { CodeEditor } from '@/components/new-landing/TabbedCodeExample';
import { File, getFiles } from '../recipes';
import { InstantApp } from '@/lib/types';
import config from '@/lib/config';
import { ErrorBoundary } from '@/components/ErrorBoundary';
import Head from 'next/head';
import { useInView } from 'react-intersection-observer';
-import { useEffect, useRef, useState } from 'react';
+import { ComponentType, useEffect, useRef, useState } from 'react';
import { useRouter } from 'next/router';
import { useIsHydrated } from '@/lib/hooks/useIsHydrated';
import {
@@ -14,15 +14,38 @@ import {
init,
} from '@instantdb/react';
import { errorToast } from '@/lib/toast';
-import {
- H3,
- LandingContainer,
- LandingFooter,
- MainNav,
-} from '@/components/marketingUi';
-import { useAuthToken } from '@/lib/auth';
+import { MainNav } from '@/components/marketingUi';
import * as og from '@/lib/og';
import { Toaster } from '@instantdb/components';
+import { Footer } from '@/components/new-landing/Footer';
+import { TopWash } from '@/components/new-landing/TopWash';
+import { Section } from '@/components/new-landing/Section';
+import { SectionTitle } from '@/components/new-landing/typography';
+import { CopyToClipboardButton } from '@/components/new-landing/CopyToClipboardButton';
+import { BrowserChrome } from '@/components/BrowserChrome';
+
+import { RecipeDBProvider } from '@/lib/recipes/db';
+import InstantTodos from '@/lib/recipes/todos';
+import InstantAuth from '@/lib/recipes/auth';
+import InstantCursors from '@/lib/recipes/cursors';
+import InstantCustomCursors from '@/lib/recipes/custom-cursors';
+import InstantTopics from '@/lib/recipes/reactions';
+import InstantTypingIndicator from '@/lib/recipes/typing-indicator';
+import InstantAvatarStack from '@/lib/recipes/avatar-stack';
+import InstantMergeTileGame from '@/lib/recipes/merge-tile-game';
+
+const recipeComponents: Record
= {
+ todos: InstantTodos,
+ auth: InstantAuth,
+ cursors: InstantCursors,
+ 'custom-cursors': InstantCustomCursors,
+ reactions: InstantTopics,
+ 'typing-indicator': InstantTypingIndicator,
+ 'avatar-stack': InstantAvatarStack,
+ 'merge-tile-game': InstantMergeTileGame,
+};
+
+const MAX_COLUMNS = 5;
export async function getStaticProps() {
const files = getFiles();
@@ -44,7 +67,6 @@ export default function Page({ files }: { files: File[] }) {
function Main({ files }: { files: File[] }) {
const router = useRouter();
- const isAuthed = !!useAuthToken();
const isHydrated = useIsHydrated();
const recipesContainerElRef = useRef(null);
const { ref: topInViewRef } = useInView({
@@ -59,20 +81,21 @@ function Main({ files }: { files: File[] }) {
});
const [selectedExample, setSelectedExample] = useState();
const [appId, setAppId] = useState(undefined);
- const dbRef = useRef<{ appId: string; db: InstantDB }>();
-
- useEffect(() => {
- if (!appId) return;
- if (dbRef.current && dbRef.current.appId === appId) return;
-
- dbRef.current = {
- db: init({
- ...config,
- appId,
- }),
- appId,
- };
- }, [appId]);
+ const columnDbsRef = useRef([]);
+
+ function getColumnDb(appId: string, index: number): InstantDB {
+ while (columnDbsRef.current.length <= index) {
+ const i = columnDbsRef.current.length;
+ columnDbsRef.current.push(
+ init({
+ ...config,
+ appId,
+ __extraDedupeKey: `recipes-col-${i}`,
+ } as any),
+ );
+ }
+ return columnDbsRef.current[index];
+ }
useEffect(() => {
jumpToExample();
@@ -152,7 +175,7 @@ function Main({ files }: { files: File[] }) {
}
return (
-
+
Instant Recipes
- {dbRef.current ? (
-
- ) : null}
-
-
-
-
-
Instant Code Recipes
-
-
- Each example is a self-contained Instant app that you can copy
- and paste into your own projects.
+ {appId ? : null}
+
+
+ {/* Hero */}
+
+
+
+
+
Recipes
+
+ With the right abstractions, you and your agents can make a lot of
+ progress with a lot less code. Take a look at some of what's
+ possible below.
+
+
+
+ P.S we made an Instant app just for you! Share this with your
+ friends and you can play with every example together.
- {isHydrated && !isAuthed && (
-
- To get rolling, create a free account, grab your app ID, and
- install{' '}
-
- @instantdb/react
-
- .
-
- )}
-
- {isHydrated && !isAuthed && (
-
-
-
+
+
+ {isHydrated && appId ? recipesUrl(appId) : 'Loading...'}
+
+
- )}
-
-
-
-
-
- Psst... this is a realtime page! 🔥
-
-
- We created a full-fledged Instant app just for you. Share this
- page's unique URL with your friends, and you'll see them in the
- previews below!
-
-
-
- Please note: this app will automatically expire
- and be deleted in 2 weeks.
-
+
+
+
+
{files.map((file, i) => {
return (
{
if (!isHydrated || !router.isReady) return;
if (!inView) return;
@@ -247,19 +243,21 @@ function Main({ files }: { files: File[] }) {
})}
-
-
+
+
);
}
function Example({
file,
appId,
+ getColumnDb,
onViewChange,
lazy,
}: {
file: File;
appId: string | undefined;
+ getColumnDb: (appId: string, index: number) => InstantDB;
onViewChange: (inView: boolean) => void;
lazy: boolean;
}) {
@@ -275,74 +273,103 @@ function Example({
},
});
+ const RecipeComponent = recipeComponents[file.pathName];
+
return (
-
-
-
{file.name}
-
-
-
- {numViews}{' '}
- previews
+
+
{file.name}
+
+ {file.code.split('\n').length} lines
+
+
+
+ {numViews} {numViews === 1 ? 'preview' : 'previews'}
-
-
+
+
+
+
-
-
-
+
+
+ {/* Code panel — relative wrapper so grid row is sized by previews only */}
+
+
+
+
+ {file.fileName}
+
+
+
+
+
+
+
+
-
- {Array(numViews)
- .fill(null)
- .map((_, i) => (
-
- {appId ? (
-
+
+ {/* Preview panels */}
+
+ {Array(numViews)
+ .fill(null)
+ .map((_, i) => (
+
0 ? '-12px' : '0',
+ transform:
+ i % 2 === 1 ? 'translateX(8px)' : 'translateX(-8px)',
+ position: 'relative',
+ zIndex: i,
+ }}
+ >
+
+
+ {appId && RecipeComponent ? (
+
(
+
+ Error loading preview
+
+ )}
+ >
+
+
+
+
) : (
-
+
)}
- ))}
-
+
+ ))}
diff --git a/client/www/pages/recipes/1-todos.tsx b/client/www/pages/recipes/1-todos.tsx
deleted file mode 100644
index 03147cd26d..0000000000
--- a/client/www/pages/recipes/1-todos.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-import config from '@/lib/config'; // hide-line
-import { id, init } from '@instantdb/react';
-
-const db = init({
- ...config, // hide-line
- appId: __getAppId(),
-});
-
-export default function InstantTodos() {
- const { data, isLoading, error } = db.useQuery({
- todos: {},
- });
-
- if (error)
- return
Oops, something broke
;
-
- return (
-
-
InsTodo
-
- {isLoading ? (
-
Loading...
- ) : data?.todos.length ? (
-
- ) : (
-
No todos!
- )}
-
- );
-}
diff --git a/client/www/pages/recipes/2-auth.tsx b/client/www/pages/recipes/2-auth.tsx
deleted file mode 100644
index e880ed5ef8..0000000000
--- a/client/www/pages/recipes/2-auth.tsx
+++ /dev/null
@@ -1,113 +0,0 @@
-import config from '@/lib/config'; // hide-line
-import { init } from '@instantdb/react';
-import { useState } from 'react';
-
-const db = init({
- ...config, // hide-line
- appId: __getAppId(),
-});
-
-export default function InstantAuth() {
- const { isLoading, user, error } = db.useAuth();
-
- if (isLoading) {
- return
Loading...
;
- }
-
- if (error) {
- return
Uh oh! {error.message}
;
- }
-
- if (user) {
- return
Hello {user.email}!
;
- }
-
- return
;
-}
-
-function Login() {
- const [state, setState] = useState({
- sentEmail: '',
- email: '',
- error: null,
- code: '',
- });
-
- const { sentEmail, email, code, error } = state;
-
- if (!sentEmail) {
- return (
-
- );
- }
-
- return (
-
- );
-}
-
-const cls = {
- root: 'flex max-w-xs mx-auto flex-col gap-3 items-center h-screen px-2 pt-12',
- heading: 'text-lg font-bold',
- input: 'py-1 border-gray-300 rounded-sm w-full',
- button: 'bg-blue-500 text-white px-3 py-1 rounded-sm w-full',
- error: 'text-red-700 text-sm bg-red-50 border-red-500 border p-2',
-};
diff --git a/client/www/pages/recipes/3-cursors.tsx b/client/www/pages/recipes/3-cursors.tsx
deleted file mode 100644
index df4a25f7a0..0000000000
--- a/client/www/pages/recipes/3-cursors.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import config from '@/lib/config'; // hide-line
-import { Cursors, init } from '@instantdb/react';
-
-const db = init({
- ...config, // hide-line
- appId: __getAppId(),
-});
-
-const room = db.room('cursors-example', '123');
-
-export default function InstantCursors() {
- return (
-
- Move your cursor around! ✨
-
- );
-}
-
-const randomDarkColor =
- '#' +
- [0, 0, 0]
- .map(() =>
- Math.floor(Math.random() * 200)
- .toString(16)
- .padStart(2, '0'),
- )
- .join('');
-
-const cursorsClassNames =
- 'flex h-screen w-screen items-center justify-center overflow-hidden font-mono text-sm text-gray-800 touch-none';
diff --git a/client/www/pages/recipes/4-custom-cursors.tsx b/client/www/pages/recipes/4-custom-cursors.tsx
deleted file mode 100644
index 3c0e7e13ec..0000000000
--- a/client/www/pages/recipes/4-custom-cursors.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import config from '@/lib/config'; // hide-line
-import { Cursors, init } from '@instantdb/react';
-
-const db = init({
- ...config, // hide-line
- appId: __getAppId(),
-});
-
-const room = db.room('cursors-example', '124');
-
-function CustomCursor({ color, name }: { color?: string; name: string }) {
- return (
-
- {name}
-
- );
-}
-
-export default function InstantCursors() {
- db.rooms.useSyncPresence(room, {
- name: userId,
- });
-
- return (
-
(
-
- )}
- userCursorColor={randomDarkColor}
- className={cursorsClassNames}
- >
- Move your cursor around! ✨
-
- );
-}
-
-const userId = Math.random().toString(36).slice(2, 6);
-
-const randomDarkColor =
- '#' +
- [0, 0, 0]
- .map(() =>
- Math.floor(Math.random() * 200)
- .toString(16)
- .padStart(2, '0'),
- )
- .join('');
-
-const cursorsClassNames =
- 'flex h-screen w-screen items-center justify-center overflow-hidden font-mono text-sm text-gray-800';
diff --git a/client/www/pages/recipes/6-typing-indicator.tsx b/client/www/pages/recipes/6-typing-indicator.tsx
deleted file mode 100644
index cd964d3d38..0000000000
--- a/client/www/pages/recipes/6-typing-indicator.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-import config from '@/lib/config'; // hide-line
-import { init } from '@instantdb/react';
-
-const db = init({
- ...config, // hide-line
- appId: __getAppId(),
-});
-
-const userId = Math.random().toString(36).slice(2, 6);
-const randomDarkColor =
- '#' +
- [0, 0, 0]
- .map(() =>
- Math.floor(Math.random() * 200)
- .toString(16)
- .padStart(2, '0'),
- )
- .join('');
-const user = {
- id: userId,
- name: `${userId}`,
- color: randomDarkColor,
-};
-
-const room = db.room('typing-indicator-example', '1234');
-
-export default function InstantTypingIndicator() {
- db.rooms.useSyncPresence(room, user);
-
- const presence = db.rooms.usePresence(room);
-
- const { active, inputProps } = db.rooms.useTypingIndicator(
- room,
- 'chat-input',
- );
-
- const peers = Object.values(presence.peers).filter((p) => p.id);
- const activeMap = Object.fromEntries(
- active.map((activePeer) => [activePeer.id, activePeer]),
- );
-
- return (
-
-
- {peers.map((peer) => {
- return (
-
- {peer.name?.slice(0, 1)}
- {activeMap[peer.id] ? (
-
- ⋯
-
- ) : null}
-
- );
- })}
-
-
-
- );
-}
-
-function typingInfo(typing: { name: string }[]) {
- if (typing.length === 0) return null;
- if (typing.length === 1) return `${typing[0].name} is typing...`;
- if (typing.length === 2)
- return `${typing[0].name} and ${typing[1].name} are typing...`;
-
- return `${typing[0].name} and ${typing.length - 1} others are typing...`;
-}
diff --git a/client/www/pages/recipes/7-avatar-stack.tsx b/client/www/pages/recipes/7-avatar-stack.tsx
deleted file mode 100644
index cbb371f8d0..0000000000
--- a/client/www/pages/recipes/7-avatar-stack.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-import config from '@/lib/config'; // hide-line
-import { init } from '@instantdb/react';
-
-const db = init({
- ...config, // hide-line
- appId: __getAppId(),
-});
-
-const room = db.room('avatars-example', 'avatars-example-1234');
-
-const userId = Math.random().toString(36).slice(2, 6);
-const randomDarkColor =
- '#' +
- [0, 0, 0]
- .map(() =>
- Math.floor(Math.random() * 200)
- .toString(16)
- .padStart(2, '0'),
- )
- .join('');
-
-export default function InstantAvatarStack() {
- const presence = room.usePresence({
- user: true,
- });
-
- db.rooms.useSyncPresence(room, {
- name: userId,
- color: randomDarkColor,
- });
-
- return (
-
- {presence.user ? (
-
- ) : null}
- {Object.entries(presence.peers).map(([id, peer]) => (
-
- ))}
-
- );
-}
-
-function Avatar({ name, color }: { name: string; color: string }) {
- return (
-
- {name?.slice(0, 1)}
-
- {name}
-
-
- );
-}
-
-const avatarClassNames =
- 'group relative select-none h-10 w-10 bg-gray-50 border border-4 border-black user-select rounded-full first:ml-0 flex justify-center items-center -ml-2 first:ml-0 relative';
diff --git a/client/www/pages/recipes/8-merge-tile-game.tsx b/client/www/pages/recipes/8-merge-tile-game.tsx
deleted file mode 100644
index 3c29511d05..0000000000
--- a/client/www/pages/recipes/8-merge-tile-game.tsx
+++ /dev/null
@@ -1,154 +0,0 @@
-/*
- * Tile Game!
- * This example is meant to mimic a simple collaborative game. We use a 4x4 grid
- * that users can color. We use `merge` to update a slice of data without
- * overwriting potential changes from other clients.
- * */
-
-import config from '@/lib/config'; // hide-line
-import { init } from '@instantdb/react';
-import { useEffect, useState } from 'react';
-
-const db = init({
- ...config, // hide-line
- appId: __getAppId(),
-});
-
-const room = db.room('main');
-
-export default function App() {
- const [hoveredSquare, setHoveredSquare] = useState(null as string | null);
- const [myColor, setMyColor] = useState(null as string | null);
- const { isLoading, error, data } = db.useQuery({ boards: {} });
- const {
- user: myPresence,
- peers,
- publishPresence,
- isLoading: isPresenceLoading,
- } = db.rooms.usePresence(room);
-
- const boardState = data?.boards.find((b) => b.id === boardId)?.state;
-
- useEffect(() => {
- if (isLoading || isPresenceLoading) return;
- if (error) return;
-
- // If the board doesn't exist, create it
- if (!boardState) {
- db.transact([
- db.tx.boards[boardId].update({
- state: makeEmptyBoard(),
- }),
- ]);
- }
-
- // If I don't have a color, generate one and publish it
- // make sure to not choose a color that a peer has already chosen
- if (!myColor) {
- const takenColors = new Set(Object.values(peers).map((p) => p.color));
- const availableColors = colors.filter((c) => !takenColors.has(c));
- const color =
- availableColors[Math.floor(Math.random() * availableColors.length)] ||
- defaultColor;
- setMyColor(color);
- publishPresence({ color });
- }
- }, [isLoading, isPresenceLoading, error, myColor]);
-
- if (!boardState || isLoading || isPresenceLoading)
- return
Loading...
;
-
- if (error) return
Error: {error.message}
;
-
- return (
-
-
-
-
-
- Others:
- {Object.entries(peers).map(([peerId, presence]) => (
-
- ))}
-
-
-
- {Array.from({ length: boardSize }).map((row, r) => (
-
- {Array.from({ length: boardSize }).map((sq, c) => (
-
setHoveredSquare(`${r}-${c}`)}
- onMouseLeave={() => setHoveredSquare(null)}
- onClick={() => {
- db.transact([
- db.tx.boards[boardId].merge({
- state: {
- [`${r}-${c}`]: myColor,
- },
- }),
- ]);
- }}
- >
- ))}
-
- ))}
-
-
-
-
- );
-}
-
-const boardSize = 4;
-const whiteColor = '#ffffff';
-const defaultColor = whiteColor;
-const colors = [
- '#ff0000', // Red
- '#00ff00', // Green
- '#0000ff', // Blue
- '#ffff00', // Yellow
- '#ff00ff', // Purple
- '#ffa500', // Orange
-];
-// singleton ID
-const boardId = '83c059e2-ed47-42e5-bdd9-6de88d26c521';
-
-function makeEmptyBoard() {
- const emptyBoard: Record
= {};
- for (let r = 0; r < boardSize; r++) {
- for (let c = 0; c < boardSize; c++) {
- emptyBoard[`${r}-${c}`] = whiteColor;
- }
- }
-
- return emptyBoard;
-}
diff --git a/client/www/pages/status.tsx b/client/www/pages/status.tsx
index 9d0cc4b727..df58fd1adf 100644
--- a/client/www/pages/status.tsx
+++ b/client/www/pages/status.tsx
@@ -3,17 +3,13 @@
// frequently times out in our 15 second function deadline.
import Head from 'next/head';
-import {
- LandingContainer,
- LandingFooter,
- MainNav,
- TextLink,
-} from '@/components/marketingUi';
+import { LandingContainer, MainNav, TextLink } from '@/components/marketingUi';
import * as og from '@/lib/og';
import styles from '@/styles/status.module.css';
import type { InferGetServerSidePropsType, GetServerSideProps } from 'next';
import type { UptimeResponse, Monitor } from '@/lib/uptimeAPI';
import * as uptimeAPI from '@/lib/uptimeAPI';
+import { Footer } from '@/components/new-landing/Footer';
export const getServerSideProps = (async (ctx) => {
// This is considered `fresh` for 10 the next 10 seconds.
@@ -329,7 +325,7 @@ export default function Page({
-
+
);
diff --git a/client/www/pages/terms.tsx b/client/www/pages/terms.tsx
index 23c81450ca..50c1031d5f 100644
--- a/client/www/pages/terms.tsx
+++ b/client/www/pages/terms.tsx
@@ -1,9 +1,6 @@
import Head from 'next/head';
-import {
- LandingContainer,
- LandingFooter,
- MainNav,
-} from '@/components/marketingUi';
+import { LandingContainer, MainNav } from '@/components/marketingUi';
+import { Footer } from '@/components/new-landing/Footer';
function TermsContent() {
return (
@@ -176,7 +173,7 @@ export default function Page() {
-
+
);
diff --git a/client/www/pages/tutorial.tsx b/client/www/pages/tutorial.tsx
index 78f8073c8f..839da3d1e4 100644
--- a/client/www/pages/tutorial.tsx
+++ b/client/www/pages/tutorial.tsx
@@ -1,32 +1,40 @@
import React, { useState } from 'react';
import Head from 'next/head';
-import { TabGroup, TabList, TabPanels, TabPanel, Tab } from '@headlessui/react';
+import { MainNav } from '@/components/marketingUi';
+import { Section } from '@/components/new-landing/Section';
import {
- Section,
- MainNav,
- LandingFooter,
- LandingContainer,
- PageProgressBar,
- H2,
- H3,
-} from '@/components/marketingUi';
-import { SubsectionHeading } from '@/components/ui';
+ SectionTitle,
+ SectionSubtitle,
+ Subheading,
+ LandingButton,
+} from '@/components/new-landing/typography';
+import { Footer } from '@/components/new-landing/Footer';
+import { TopWash } from '@/components/new-landing/TopWash';
+import { XIcon } from '@/components/new-landing/icons';
+import { AnimateIn } from '@/components/new-landing/AnimateIn';
import RatingBox from '@/components/docs/RatingBox';
import useLocalStorage from '@/lib/hooks/useLocalStorage';
import clsx from 'clsx';
import { CheckIcon, ClipboardDocumentIcon } from '@heroicons/react/24/solid';
import CopyToClipboard from 'react-copy-to-clipboard';
+import { AnimatePresence, motion } from 'motion/react';
+import { rosePineDawnColors as c } from '@/lib/rosePineDawnTheme';
+
+// -----------
+// Data
+// -----------
const overviewSteps = [
'Login to Instant in the terminal',
'Scaffold a starter Instant app',
'Prompt an LLM to build us an app (This is the fun part!)',
+ 'Deploy the app to Vercel',
];
const packageManagers = [
- { id: 'npx', name: 'npx', runner: 'npx' },
- { id: 'pnpx', name: 'pnpx', runner: 'pnpx' },
- { id: 'bunx', name: 'bunx', runner: 'bunx' },
+ { id: 'npm', name: 'npx', runner: 'npx', scriptRunner: 'npm' },
+ { id: 'pnpm', name: 'pnpx', runner: 'pnpx', scriptRunner: 'pnpm' },
+ { id: 'bun', name: 'bunx', runner: 'bunx', scriptRunner: 'bun' },
] as const;
const examplePrompts = [
@@ -47,7 +55,12 @@ const examplePrompts = [
},
];
-const debuggingItems = [
+const debuggingItems: {
+ id: string;
+ title: string;
+ content: React.ReactNode;
+ videoUrl?: string;
+}[] = [
{
id: 'general-troubleshooting',
title: 'General troubleshooting',
@@ -59,8 +72,16 @@ const debuggingItems = [
If you encounter an issue not listed below please feel free to let us
- know via the feedback tool at the bottom of this page or via our
- Discord.
+ know via the feedback tool at the bottom of this page or via our{' '}
+
+ Discord
+
+ .
),
@@ -142,102 +163,218 @@ const debuggingItems = [
},
];
-function CopyButton({ command, label }: { command: string; label?: string }) {
- const [showCopySuccess, setShowCopySuccess] = useState(false);
+// -----------
+// Reusable components
+// -----------
+
+function ConfettiParticle({
+ delay,
+ x,
+ color,
+}: {
+ delay: number;
+ x: number;
+ color: string;
+}) {
+ return (
+
+ );
+}
+
+const confettiColors = [
+ '#F97316',
+ '#FB923C',
+ '#3B82F6',
+ '#A855F7',
+ '#EC4899',
+ '#10B981',
+];
+
+function TerminalCopyButton({ text }: { text: string }) {
+ const [copied, setCopied] = useState(false);
return (
{
- setShowCopySuccess(true);
- setTimeout(() => setShowCopySuccess(false), 2000);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
}}
>
-
- {showCopySuccess ? (
-
- ) : (
-
+
+
);
}
-function PackageManagerSelector({
- commandTemplate,
+function PackageManagerTabs({
+ selectedPmIndex,
+ onPmChange,
}: {
- commandTemplate: string;
+ selectedPmIndex: number;
+ onPmChange: (index: number) => void;
}) {
- const [selectedIndex, setSelectedIndex] = useLocalStorage
(
- 'package-manager-index',
- 0,
+ return (
+
+ {packageManagers.map((pm, i) => {
+ return (
+
+ );
+ })}
+
);
+}
- const currentCommand = `${packageManagers[selectedIndex].runner} ${commandTemplate}`;
+function TerminalBlock({
+ commandTemplate,
+ type = 'runner',
+ selectedPmIndex,
+ onPmChange,
+}: {
+ commandTemplate: string;
+ type?: 'runner' | 'script';
+ selectedPmIndex: number;
+ onPmChange?: (index: number) => void;
+}) {
+ const pm = packageManagers[selectedPmIndex];
+ const prefix = type === 'script' ? pm.scriptRunner : pm.runner;
+ const command = `${prefix} ${commandTemplate}`;
return (
-
-
-
-
- {packageManagers.map((pm) => (
-
- clsx(
- 'w-full rounded-lg py-2.5 text-sm font-medium transition-all',
- 'ring-opacity-60 ring-white ring-offset-2 ring-offset-gray-400 focus:ring-2 focus:outline-hidden',
- selected
- ? 'bg-white text-gray-900 shadow-md'
- : 'text-gray-600 hover:bg-gray-100 hover:text-gray-900',
- )
- }
- >
- {pm.name}
-
- ))}
-
-
-
-
- {packageManagers.map((pm) => (
-
-
- {currentCommand}
-
-
-
- ))}
-
+
+ {onPmChange && (
+
+ )}
+
);
}
-function PromptExample({ title, content }: { title: string; content: string }) {
+function TabbedPrompts() {
+ const [activeTab, setActiveTab] = useState(0);
+ const prompt = examplePrompts[activeTab];
+
return (
-
-
- {title}
-
-
-
- {content}
+
+
+
+ {examplePrompts.map((p, i) => (
+
+ ))}
+
+
+
+
+ {prompt.content}
+
+
);
}
-function DebuggingAccordion() {
+function DebuggingSection() {
const [openItems, setOpenItems] = useState
>(new Set());
const toggleItem = (id: string) => {
@@ -251,167 +388,111 @@ function DebuggingAccordion() {
};
return (
-
- {debuggingItems.map((item) => {
- const isOpen = openItems.has(item.id);
- return (
-
-
- {isOpen && (
-
-
- {item.content}
- {item.videoUrl && (
-