From b2aabb35c702864f9b4da5fd1c08173b0f0a841b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 27 Feb 2026 14:20:36 +0000 Subject: [PATCH 1/7] Add AGENTS.md with Cursor Cloud development instructions Co-authored-by: Leo --- AGENTS.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d8dfc32 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,29 @@ +# AGENTS.md + +## Cursor Cloud specific instructions + +This is a **Next.js 14** marketing website for UseEfficiently (an Airtable consulting company). It is a single-package project — not a monorepo. + +### Key facts + +- **Package manager**: npm (lockfile: `package-lock.json`) +- **No database, no environment variables, no Docker** — all content is hardcoded in `use.js` and `information.json`. +- **No automated test suite** — there are no unit/integration tests configured. The only check is ESLint via `npm run lint`. + +### Common commands + +See `package.json` scripts. Summary: + +| Task | Command | +|------|---------| +| Install deps | `npm install` | +| Dev server | `npm run dev` (runs on port 3000) | +| Production build | `npm run build` | +| Lint | `npm run lint` | + +### Non-obvious notes + +- The dev server (`npm run dev`) supports hot reload. No restart needed after code changes. +- Remote images from `cdn.useefficiently.com`, `dl.airtable.com`, `github.com`, and `picsum.photos` are configured in `next.config.mjs`. They require network access to load but the app still renders without them. +- The `/schedule-meeting` page embeds a Fillout form that requires external network access to function. +- `tsconfig.json` includes a likely typo entry `config.tss` in the `include` array — this does not break the build. From f31e785886e6e5869e4b5a68c2f5e4ce9b89ec81 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 27 Feb 2026 14:38:35 +0000 Subject: [PATCH 2/7] Refactor website: Dieter Rams inspired storytelling design with shadcn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace dark violet theme with warm stone-based light palette - Set up shadcn/ui infrastructure (Button, Card in components/ui/) - Create custom components: FadeIn (scroll animations), Navbar, SiteFooter - Rewrite homepage as storytelling narrative: Hero → Philosophy → Solutions → Stats → Stories → Testimonials → CTA - Clean solutions grid with gap-px border technique - Customer stories now link to dedicated /[slug] pages - Rewrite About page with clean team member layout - Update [slug] page with prose-stone article styling - Delete old components (block, button, header, footer, modal, popover, textRotate) - Keep data files (use.js, information.json) and fillout integration unchanged Co-authored-by: Leo --- app/[slug]/page.tsx | 82 ++++--- app/about/page.tsx | 139 +++++------ app/globals.css | 49 ++-- app/layout.tsx | 34 +-- app/page.tsx | 370 ++++++++++++++++++------------ app/schedule-meeting/page.tsx | 27 +-- components.json | 20 ++ components/block.tsx | 68 ------ components/button.tsx | 147 ------------ components/custom/fade-in.tsx | 46 ++++ components/custom/navbar.tsx | 36 +++ components/custom/site-footer.tsx | 52 +++++ components/footer.tsx | 40 ---- components/header.tsx | 33 --- components/modal.tsx | 71 ------ components/popover.tsx | 52 ----- components/textRotate.tsx | 36 --- components/ui/button.tsx | 57 +++++ components/ui/card.tsx | 83 +++++++ package-lock.json | 54 ++++- package.json | 1 + tailwind.config.ts | 54 ++++- 22 files changed, 790 insertions(+), 761 deletions(-) create mode 100644 components.json delete mode 100644 components/block.tsx delete mode 100644 components/button.tsx create mode 100644 components/custom/fade-in.tsx create mode 100644 components/custom/navbar.tsx create mode 100644 components/custom/site-footer.tsx delete mode 100644 components/footer.tsx delete mode 100644 components/header.tsx delete mode 100644 components/modal.tsx delete mode 100644 components/popover.tsx delete mode 100644 components/textRotate.tsx create mode 100644 components/ui/button.tsx create mode 100644 components/ui/card.tsx diff --git a/app/[slug]/page.tsx b/app/[slug]/page.tsx index dd179b6..7462e6f 100644 --- a/app/[slug]/page.tsx +++ b/app/[slug]/page.tsx @@ -2,7 +2,7 @@ import { Metadata } from "next"; import information from "@/information.json"; import Link from "next/link"; import { customers } from "@/use"; -import { notFound, redirect } from "next/navigation"; +import { redirect } from "next/navigation"; type Props = { params: { @@ -13,55 +13,79 @@ type Props = { export const generateMetadata = async ({ params, }: Props): Promise => { - const { metaTitle, description } = customers.find( - (content) => content.slug === params.slug - ) || { metaTitle: "", description: "" }; + const customer = customers.find((c) => c.slug === params.slug); + const metaTitle = customer?.metaTitle ?? ""; + const description = customer?.description ?? ""; - const metadata = { + return { title: metaTitle, - description: description, + description, openGraph: { title: metaTitle, - description: description, + description, url: information.website, type: "website", images: [ { - url: information.website + "/api/og?title=" + metaTitle, - alt: information.company + " Logo", + url: `${information.website}/api/og?title=${metaTitle}`, + alt: `${information.company} Logo`, }, ], }, - twitter: { card: "summary_large_image", - site: "@" + information.slug, + site: `@${information.slug}`, title: metaTitle, - description: description, - images: information.website + "/api/og?title=" + metaTitle, + description, + images: `${information.website}/api/og?title=${metaTitle}`, }, }; - - return metadata; }; export default function CustomerStory({ params }: Props) { - const { title, detail } = customers.find( - (content) => content.slug === params.slug - ) || redirect("/"); + const customer = customers.find((c) => c.slug === params.slug); + if (!customer) redirect("/"); + return ( - <> -
- -
+
+
+ + ← Back to stories + + +

+ {customer.name} +

+ + {customer.description && ( +

+ {customer.description} +

+ )} + + {customer.detail && ( +
+ )} -
-
- +
+ + ← Back to all stories + +
- +
); } diff --git a/app/about/page.tsx b/app/about/page.tsx index ae04237..b3ab459 100644 --- a/app/about/page.tsx +++ b/app/about/page.tsx @@ -1,111 +1,86 @@ -import Link from "next/link"; -import { SiLinkedin } from "react-icons/si"; import Image from "next/image"; +import Link from "next/link"; import { team } from "@/use"; -import { Button } from "@/components/button"; import information from "@/information.json"; +import FadeIn from "@/components/custom/fade-in"; import { Metadata } from "next"; export async function generateMetadata(): Promise { const title = "About"; - const description = "Contact us for any questions or feedback you may have."; + const description = "Meet the team behind UseEfficiently."; - const metadata = { - title: title, - description: description, + return { + title, + description, openGraph: { - title: title, - description: description, + title, + description, url: information.website, type: "website", images: [ { - url: - information.website + - "/api/og?title=" + - title + - " | UseEfficiently", - alt: information.company + " Logo", + url: `${information.website}/api/og?title=${title} | UseEfficiently`, + alt: `${information.company} Logo`, }, ], }, - twitter: { card: "summary_large_image", - site: "@" + information.slug, - title: title, - description: description, - images: information.website + "/api/og?title=" + title, + site: `@${information.slug}`, + title, + description, + images: `${information.website}/api/og?title=${title}`, }, }; - - return metadata; } + export default function Page() { return ( -
-

- Team UseEfficiently -

-
- {team.map((member) => ( - - ))} - {/* */} + + ))} +
); diff --git a/app/globals.css b/app/globals.css index e1c223a..cd52b9a 100755 --- a/app/globals.css +++ b/app/globals.css @@ -2,30 +2,45 @@ @tailwind components; @tailwind utilities; -.modal *, -.article * { - color: #f5f3ff !important; -} +@layer base { + :root { + --background: 60 9% 98%; + --foreground: 24 10% 10%; + --card: 0 0% 100%; + --card-foreground: 24 10% 10%; + --popover: 0 0% 100%; + --popover-foreground: 24 10% 10%; + --primary: 24 10% 10%; + --primary-foreground: 60 9% 98%; + --secondary: 60 5% 96%; + --secondary-foreground: 24 10% 10%; + --muted: 60 5% 96%; + --muted-foreground: 25 6% 45%; + --accent: 60 5% 96%; + --accent-foreground: 24 10% 10%; + --destructive: 0 84% 60%; + --destructive-foreground: 60 9% 98%; + --border: 20 6% 90%; + --input: 20 6% 90%; + --ring: 24 6% 83%; + --radius: 0.5rem; + } -.modal h1, -.modal h2, -.modal h3, -.modal h4, -.article h1, -.article h2, -.article h3, -.article h4 { - color: #ddd6fe !important; + * { + @apply border-border; + } + + body { + @apply bg-background text-foreground; + } } @layer utilities { - /* Hide scrollbar for Chrome, Safari and Opera */ .no-scrollbar::-webkit-scrollbar { display: none; } - /* Hide scrollbar for IE, Edge and Firefox */ .no-scrollbar { - -ms-overflow-style: none; /* IE and Edge */ - scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; + scrollbar-width: none; } } diff --git a/app/layout.tsx b/app/layout.tsx index 3d0a2d6..572173b 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -5,13 +5,11 @@ import { cn } from "@/lib/utils"; import { Inter } from "next/font/google"; import information from "@/information.json"; import { GoogleAnalytics } from "@next/third-parties/google"; +import type { Metadata } from "next"; +import Navbar from "@/components/custom/navbar"; +import SiteFooter from "@/components/custom/site-footer"; -import "@/app/globals.css"; const inter = Inter({ subsets: ["latin"] }); -import type { Metadata } from "next"; -import Header from "@/components/header"; -import Footer from "@/components/footer"; -import Modal from "@/components/modal"; export const metadata: Metadata = { robots: "index, follow", @@ -46,12 +44,6 @@ export const metadata: Metadata = { sizes: "16x16", url: "/favicon-16x16.png", }, - { - rel: "icon", - type: "image/png", - sizes: "16x16", - url: "/favicon-16x16.png", - }, { rel: "icon", type: "image/x-icon", @@ -71,7 +63,7 @@ export const metadata: Metadata = { description: "At UseEfficiently, our team of experts is here to help you master Airtable and use it efficiently to meet all your needs.", keywords: - "Airtable, No-Code Solutions, Accredited Airtable Services Partner, UseEfficiently, Business Solutions,Airtable Interfaces, Airtable Automations, Airtable Team", + "Airtable, No-Code Solutions, Accredited Airtable Services Partner, UseEfficiently, Business Solutions, Airtable Interfaces, Airtable Automations, Airtable Team", openGraph: { title: { default: information.title, @@ -88,7 +80,6 @@ export const metadata: Metadata = { }, ], }, - twitter: { card: "summary_large_image", site: "@" + information.slug, @@ -100,7 +91,9 @@ export const metadata: Metadata = { "At UseEfficiently, our team of experts is here to help you master Airtable and use it efficiently to meet all your needs.", images: information.website + "/api/og", }, - authors: [{ name: information.company + " Team", url: information.website }], + authors: [ + { name: information.company + " Team", url: information.website }, + ], }; export default function RootLayout({ @@ -109,19 +102,16 @@ export default function RootLayout({ children: React.ReactNode; }) { return ( - + -
-
-
{children}
-
-
- + +
{children}
+ diff --git a/app/page.tsx b/app/page.tsx index 5db7797..3d4af97 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,164 +1,250 @@ -import { cn } from "@/lib/utils"; -import Link from "next/link"; -import { SiAirtable, SiMake, SiZapier, SiBrevo } from "react-icons/si"; import Image from "next/image"; -import { Block } from "@/components/block"; -import TextRotate from "@/components/textRotate"; +import Link from "next/link"; import { useCases, customers, testimonials } from "@/use"; import partner from "@/public/partner.svg"; -import information from "@/information.json"; -import { ButtonBlock as Button } from "@/components/button"; -import Popover from "@/components/popover"; import { FilloutButton } from "@/components/fillout"; +import FadeIn from "@/components/custom/fade-in"; export default function Page() { + const featuredCustomers = customers.filter( + (c) => c.detail && c.detail.length > 0 + ); + return ( <> -
-

- We assist brands to use technology efficiently -

-
- {/* */} - - Accredited Airtable Services Partner - - {/* */} -

- Accredited Airtable Services Partner -

-
- - Book 30-Minute Free Call + {/* ─── Hero ─── */} +
+
+

+ We help brands use +
+ technology efficiently +

+ +
+ + Accredited Airtable Services Partner + +

+ Accredited Airtable Services Partner +

+
+ +
+ + Book a Free Call - Learn more + Our Story ↓
- -
- Use - Efficiently -
-
-
-
-

- Our Airtable Based Solutions -

-
- {useCases.map((useCase) => ( - - ))} + + + {/* ─── Philosophy ─── */} +
+
+ +

+ Good technology is invisible. +

+
+ +

+ We build systems that work in the background so you can focus on + what matters. As an accredited Airtable Services Partner, we + transform complex workflows into simple, automated + processes — designed to last. +

+
-
-

- Our Customers and Testimonials -

-
-
- {customers.map((customer) => ( - - ))} + + + {/* ─── Solutions ─── */} +
+
+ +

+ Services +

+

+ What we build +

+
+ +
+
+ {useCases.map((useCase) => ( +
+

{useCase.title}

+
+
+ ))} +
+
+
-
- {testimonials.map((testimonial) => ( -
- - {testimonial.name} -
-

{testimonial.name}

-

- {testimonial.title} -

+ {/* ─── Numbers ─── */} +
+
+ +
+ {[ + { number: "200+", label: "Automations built" }, + { number: "40,000+", label: "Tasks processed monthly" }, + { number: "150,000+", label: "Applications managed annually" }, + { number: "6+", label: "Organizations transformed" }, + ].map((stat) => ( +
+

+ {stat.number} +

+

+ {stat.label} +

- - - ))} + ))} +
+
+
+
+ + {/* ─── Customer Stories ─── */} +
+
+ +

+ Case Studies +

+

+ Stories +

+
+ + +
+ {customers.map((customer) => ( + + {customer.name} + + ))} +
+
+ +
+ {featuredCustomers.slice(0, 3).map((customer, i) => ( + + +
+
+

+ {customer.name} +

+ + Read story → + +
+

+ {customer.description} +

+
+ +
+ ))} +
-
+ + + {/* ─── Testimonials ─── */} +
+
+ +

+ What our clients say +

+
+ +
+ {testimonials.map((testimonial, i) => ( + +
+
+ + {testimonial.name} +
+

+ {testimonial.name} +

+

+ {testimonial.title} +

+
+ +
+ + ))} +
+
+
+ + {/* ─── CTA ─── */} +
+
+ +

+ Ready to work efficiently? +

+

+ Let’s talk about how we can streamline your workflows. +

+
+ + Book a 30-Minute Call + +
+
+
+
); } - -const text = [ - , - , - , - , -]; diff --git a/app/schedule-meeting/page.tsx b/app/schedule-meeting/page.tsx index 4284cd4..0b19e4b 100644 --- a/app/schedule-meeting/page.tsx +++ b/app/schedule-meeting/page.tsx @@ -4,34 +4,31 @@ import FilloutEmbed from "@/components/fillout"; export async function generateMetadata(): Promise { const title = "Schedule a Meeting with UseEfficiently"; - const description = "Contact us for any questions or feedback you may have."; + const description = "Book a free 30-minute discovery call with our team."; - const metadata = { - title: title, - description: description, + return { + title, + description, openGraph: { - title: title, - description: description, + title, + description, url: information.website, type: "website", images: [ { - url: information.website + "/api/og?title=" + title, - alt: information.company + " Logo", + url: `${information.website}/api/og?title=${title}`, + alt: `${information.company} Logo`, }, ], }, - twitter: { card: "summary_large_image", - site: "@" + information.slug, - title: title, - description: description, - images: information.website + "/api/og?title=" + title, + site: `@${information.slug}`, + title, + description, + images: `${information.website}/api/og?title=${title}`, }, }; - - return metadata; } export default function Page() { diff --git a/components.json b/components.json new file mode 100644 index 0000000..b63433b --- /dev/null +++ b/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "app/globals.css", + "baseColor": "stone", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + } +} diff --git a/components/block.tsx b/components/block.tsx deleted file mode 100644 index d297064..0000000 --- a/components/block.tsx +++ /dev/null @@ -1,68 +0,0 @@ -"use client"; - -import { motion } from "framer-motion"; -import React from "react"; -import { cn } from "@/lib/utils"; - -const MotionBlock = ({ - className, - children, -}: { - className?: string; - children: React.ReactNode; -}) => { - return ( - - {children} - - ); -}; -const Block = ({ - className, - children, -}: { - className?: string; - children: React.ReactNode; -}) => { - return ( -
- {children} -
- ); -}; - -const ButtonBlock = ({ - className, - children, - onClick, -}: { - className?: string; - children: React.ReactNode; - onClick?: () => void; -}) => { - return ( - - ); -}; - -export { MotionBlock, Block, ButtonBlock }; diff --git a/components/button.tsx b/components/button.tsx deleted file mode 100644 index c3d8f47..0000000 --- a/components/button.tsx +++ /dev/null @@ -1,147 +0,0 @@ -"use client"; -import { cn } from "@/lib/utils"; -const Button = ({ - title, - description, - link, - children, - className, - key, -}: { - title: string; - description: string; - link: string; - children: any; - className: string; - key: string; -}) => { - const openModalWithMessage = ({ - title, - description, - link, - }: { - title: string; - description: string; - link: string; - }) => { - const modal = document.getElementById("modal"); - if (!modal) return; - const modalTitle = modal.querySelector(".modal-title"); - const modalDescription = modal.querySelector(".modal-description"); - const modalLink = modal.querySelector(".modal-link"); - if (!modalLink) return; - - const firstLink = modalLink.querySelector( - ".first-link" - ) as HTMLAnchorElement; - console.log(firstLink); - if (!modalTitle || !modalDescription || !modalLink || !firstLink) return; - modalTitle.textContent = title; - modalDescription.textContent = description; - if (link.length > 0) { - firstLink.classList.remove("hidden"); - firstLink.href = link; - } else firstLink.classList.add("hidden"); - modal.classList.add("flex"); - modal.classList.remove("hidden"); - setTimeout(() => { - modal.classList.replace("opacity-0", "opacity-100"); - }, 0.1); - }; - - return ( - - ); -}; - -const ButtonBlock = ({ - title, - description, - link, - children, - className, - key, -}: { - title: string; - description: string; - link: string; - children: any; - className: string; - key: string; -}) => { - const openModalWithMessage = ({ - title, - description, - link, - }: { - title: string; - description: string; - link: string; - }) => { - const modal = document.getElementById("modal"); - if (!modal) return; - const subModal = modal.querySelector(".submodal"); - const modalLink = modal.querySelector(".modal-link"); - if (!subModal || !modalLink) return; - const firstLink = modalLink.querySelector( - ".first-link" - ) as HTMLAnchorElement; - if (!firstLink) return; - const modalTitle = subModal.querySelector( - ".modal-title" - ) as HTMLHeadingElement; - const modalDescription = modal.querySelector(".modal-description"); - if (!modalTitle || !modalDescription || !modalLink || !firstLink) return; - modalTitle.textContent = title; - if (title === "") { - modalTitle.classList.add("hidden"); - } else { - modalTitle.classList.remove("hidden"); - } - modalDescription.innerHTML = description; - - if (link.length > 0) { - firstLink.classList.remove("hidden"); - firstLink.href = link; - } else firstLink.classList.add("hidden"); - modal.classList.add("flex"); - modal.classList.remove("hidden"); - setTimeout(() => { - modal.classList.replace("opacity-0", "opacity-100"); - }, 0.1); - document.body.style.overflow = "hidden"; - }; - - return ( - - ); -}; - -export { Button, ButtonBlock }; diff --git a/components/custom/fade-in.tsx b/components/custom/fade-in.tsx new file mode 100644 index 0000000..abe5861 --- /dev/null +++ b/components/custom/fade-in.tsx @@ -0,0 +1,46 @@ +"use client"; + +import { motion, useInView } from "framer-motion"; +import { useRef } from "react"; +import { cn } from "@/lib/utils"; + +interface FadeInProps { + children: React.ReactNode; + className?: string; + delay?: number; + direction?: "up" | "down" | "left" | "right" | "none"; + duration?: number; + once?: boolean; +} + +export default function FadeIn({ + children, + className, + delay = 0, + direction = "up", + duration = 0.5, + once = true, +}: FadeInProps) { + const ref = useRef(null); + const isInView = useInView(ref, { once, margin: "-80px" }); + + const directionOffset = { + up: { y: 24 }, + down: { y: -24 }, + left: { x: 24 }, + right: { x: -24 }, + none: {}, + }; + + return ( + + {children} + + ); +} diff --git a/components/custom/navbar.tsx b/components/custom/navbar.tsx new file mode 100644 index 0000000..71f9e00 --- /dev/null +++ b/components/custom/navbar.tsx @@ -0,0 +1,36 @@ +import Link from "next/link"; +import Image from "next/image"; +import use from "@/public/use.svg"; +import { FilloutButton } from "@/components/fillout"; + +export default function Navbar() { + return ( +
+ +
+ ); +} diff --git a/components/custom/site-footer.tsx b/components/custom/site-footer.tsx new file mode 100644 index 0000000..e6c6030 --- /dev/null +++ b/components/custom/site-footer.tsx @@ -0,0 +1,52 @@ +import Link from "next/link"; +import information from "@/information.json"; + +export default function SiteFooter() { + return ( +
+
+
+
+

UseEfficiently

+

+ Accredited Airtable Services Partner +

+
+
+ + LinkedIn + + + Upwork + + + Instagram + + + X + +
+
+
+

© {new Date().getFullYear()} UseEfficiently

+
+
+
+ ); +} diff --git a/components/footer.tsx b/components/footer.tsx deleted file mode 100644 index 818a6c8..0000000 --- a/components/footer.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import Link from "next/link"; -import { SiLinkedin, SiUpwork, SiInstagram, SiX } from "react-icons/si"; -import information from "@/information.json"; -export default function Footer() { - return ( -
- -
- ); -} diff --git a/components/header.tsx b/components/header.tsx deleted file mode 100644 index c817c01..0000000 --- a/components/header.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import Link from "next/link"; -import use from "@/public/use.svg"; -import Image from "next/image"; -export default function Header() { - return ( -
- -
- ); -} diff --git a/components/modal.tsx b/components/modal.tsx deleted file mode 100644 index cbf402e..0000000 --- a/components/modal.tsx +++ /dev/null @@ -1,71 +0,0 @@ -"use client"; -import { Block } from "@/components/block"; -import Link from "next/link"; -import information from "@/information.json"; -import React from "react"; -import { FilloutButton } from "@/components/fillout"; - -const Modal = () => { - const handleOverlayClick = ( - e: - | React.MouseEvent - | React.TouchEvent - | React.MouseEvent - | React.TouchEvent - | React.MouseEvent - | React.MouseEvent - ) => { - if (e.target === e.currentTarget) { - const modal = document.getElementById("modal"); - if (!modal) return; - - modal.classList.replace("opacity-100", "opacity-0"); - setTimeout(() => { - modal.classList.add("hidden"); - modal.classList.remove("flex"); - }, 300); - document.body.style.overflow = "unset"; - const subModal = modal.querySelector(".modal"); - if (!subModal) return; - subModal.scrollTop = 0; - } - 3; - }; - - return ( - - ); -}; - -export default Modal; diff --git a/components/popover.tsx b/components/popover.tsx deleted file mode 100644 index 71f3d13..0000000 --- a/components/popover.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import React, { ReactNode } from "react"; - -interface PopoverProps { - title: string; - description: string; - position: "left" | "right" | "top"; - children: ReactNode; -} - -const Popover: React.FC = ({ - title, - description, - position, - children, -}) => { - // Define position classes based on the `position` prop - const positionClasses: { [key: string]: string } = { - left: "left-0 transform -translate-x-full -translate-y-1/2", - right: "right-0 transform translate-x-full -translate-y-1/2", - top: "top-0 transform -translate-y-full -translate-x-1/2", - }; - - return ( -
- {children} -
-
{title}
-
{description}
- - {/* Popover arrow */} -
-
-
- ); -}; - -export default Popover; diff --git a/components/textRotate.tsx b/components/textRotate.tsx deleted file mode 100644 index 2724fde..0000000 --- a/components/textRotate.tsx +++ /dev/null @@ -1,36 +0,0 @@ -"use client"; -import { AnimatePresence, motion } from "framer-motion"; -import { useEffect, useState } from "react"; - -const TextRotate = ({ text }: { text: Array }) => { - const [index, setIndex] = useState(0); - - useEffect(() => { - const id = setInterval(() => { - setIndex((state) => { - if (state >= text.length - 1) return 0; - return state + 1; - }); - }, 2000); - return () => clearInterval(id); - }, [setIndex, text.length]); - - return ( -
- - - {text[index]} - - -
- ); -}; - -export default TextRotate; diff --git a/components/ui/button.tsx b/components/ui/button.tsx new file mode 100644 index 0000000..f3f13c0 --- /dev/null +++ b/components/ui/button.tsx @@ -0,0 +1,57 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const buttonVariants = cva( + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + outline: + "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-8", + icon: "h-9 w-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + return ( + + ); + } +); +Button.displayName = "Button"; + +export { Button, buttonVariants }; diff --git a/components/ui/card.tsx b/components/ui/card.tsx new file mode 100644 index 0000000..73347d3 --- /dev/null +++ b/components/ui/card.tsx @@ -0,0 +1,83 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +Card.displayName = "Card"; + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardHeader.displayName = "CardHeader"; + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)); +CardTitle.displayName = "CardTitle"; + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)); +CardDescription.displayName = "CardDescription"; + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)); +CardContent.displayName = "CardContent"; + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardFooter.displayName = "CardFooter"; + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardDescription, + CardContent, +}; diff --git a/package-lock.json b/package-lock.json index c3adc42..46235ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@fillout/react": "^3.0.0", "@next/third-parties": "^14.2.5", + "@radix-ui/react-slot": "^1.2.4", "@vercel/analytics": "^1.3.1", "@vercel/speed-insights": "^1.0.12", "airtable": "^0.12.2", @@ -550,6 +551,24 @@ } } }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", + "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-dismissable-layer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.0.tgz", @@ -706,7 +725,7 @@ } } }, - "node_modules/@radix-ui/react-slot": { + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", @@ -724,6 +743,39 @@ } } }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", diff --git a/package.json b/package.json index dcac616..33f5be8 100755 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dependencies": { "@fillout/react": "^3.0.0", "@next/third-parties": "^14.2.5", + "@radix-ui/react-slot": "^1.2.4", "@vercel/analytics": "^1.3.1", "@vercel/speed-insights": "^1.0.12", "airtable": "^0.12.2", diff --git a/tailwind.config.ts b/tailwind.config.ts index 2a0cd04..96e5468 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,17 +1,59 @@ -/** @type {import('tailwindcss').Config} */ -module.exports = { +import type { Config } from "tailwindcss"; + +const config: Config = { content: [ "./app/**/*.{js,ts,jsx,tsx,mdx}", "./pages/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}", - - // Or if using `src` directory: - "./src/**/*.{js,ts,jsx,tsx,mdx}", ], theme: { - extend: {}, + extend: { + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + }, }, plugins: [ + require("tailwindcss-animate"), require("@tailwindcss/typography"), ], }; + +export default config; From a0d50b9de859141d6a6a881b0d2cd347f83fe942 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 27 Feb 2026 14:50:56 +0000 Subject: [PATCH 3/7] Update AGENTS.md with architecture notes and dev server tip Co-authored-by: Leo --- AGENTS.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index d8dfc32..0bef971 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,9 +21,18 @@ See `package.json` scripts. Summary: | Production build | `npm run build` | | Lint | `npm run lint` | +### Architecture + +- `components/ui/` — shadcn primitives (Button, Card). Do not hand-edit; regenerate via shadcn CLI. +- `components/custom/` — custom components (FadeIn scroll animation, Navbar, SiteFooter). +- `components/fillout.tsx` — Fillout form integration (third-party embed, kept at root level). +- `use.js` / `information.json` — all content and company metadata (hardcoded, no CMS). +- Design: warm stone-based light palette with CSS variables (see `globals.css`). Dieter Rams inspired — minimal, functional, generous whitespace. + ### Non-obvious notes - The dev server (`npm run dev`) supports hot reload. No restart needed after code changes. - Remote images from `cdn.useefficiently.com`, `dl.airtable.com`, `github.com`, and `picsum.photos` are configured in `next.config.mjs`. They require network access to load but the app still renders without them. - The `/schedule-meeting` page embeds a Fillout form that requires external network access to function. - `tsconfig.json` includes a likely typo entry `config.tss` in the `include` array — this does not break the build. +- When killing the dev server, use `fuser -k 3000/tcp` rather than `lsof`/`kill` — the next-server child processes sometimes linger on the port. From 740ed03efc5ff5efb7f136b7dae2823bc2003a5a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 27 Feb 2026 15:23:13 +0000 Subject: [PATCH 4/7] Customer-focused redesign with illustrated card components - Customer-first hero: '130,000 applications. Zero manual steps.' (GIRVAK story) - ImpactCard component with 3 geometric variants (circles, dot grid, bars) - StoryCard component with 3 SVG illustration variants (nodes, waves, blocks) - PositionCard component with icon + value proposition - Featured case study section with GIRVAK metrics sidebar - 'Why Airtable?' section with real positioning (one platform, 96% cost, scale) - All content sourced from real Airtable ecosystem page and client data - CTA: 'Every automation starts with a conversation' Co-authored-by: Leo --- app/page.tsx | 346 +++++++++++++++++----------- components/custom/impact-card.tsx | 83 +++++++ components/custom/position-card.tsx | 32 +++ components/custom/story-card.tsx | 112 +++++++++ 4 files changed, 438 insertions(+), 135 deletions(-) create mode 100644 components/custom/impact-card.tsx create mode 100644 components/custom/position-card.tsx create mode 100644 components/custom/story-card.tsx diff --git a/app/page.tsx b/app/page.tsx index 3d4af97..55d3a65 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,189 +1,263 @@ import Image from "next/image"; import Link from "next/link"; -import { useCases, customers, testimonials } from "@/use"; -import partner from "@/public/partner.svg"; +import { customers, testimonials } from "@/use"; import { FilloutButton } from "@/components/fillout"; import FadeIn from "@/components/custom/fade-in"; +import ImpactCard from "@/components/custom/impact-card"; +import StoryCard from "@/components/custom/story-card"; +import PositionCard from "@/components/custom/position-card"; +import { SiAirtable } from "react-icons/si"; -export default function Page() { - const featuredCustomers = customers.filter( - (c) => c.detail && c.detail.length > 0 +function IconGrid() { + return ( + + + + ); +} + +function IconTrendDown() { + return ( + + + + ); +} + +function IconScale() { + return ( + + + ); +} + +export default function Page() { + const girvak = customers.find((c) => c.slug === "girvak"); return ( <> - {/* ─── Hero ─── */} -
-
+ {/* ─── Hero: Customer-first opening ─── */} +
+
+

+ Airtable Services Partner · Istanbul +

- We help brands use + 130,000 applications.
- technology efficiently + Zero manual steps.

- -
- - Accredited Airtable Services Partner - -

- Accredited Airtable Services Partner -

-
- -
+

+ We built the automation system behind Turkey’s largest young + talent program. Now it runs itself. +

+
Book a Free Call - Our Story ↓ + See the stories ↓
- {/* ─── Philosophy ─── */} -
-
+ {/* ─── Impact: Real numbers from real clients ─── */} +
+
-

- Good technology is invisible. -

-
- -

- We build systems that work in the background so you can focus on - what matters. As an accredited Airtable Services Partner, we - transform complex workflows into simple, automated - processes — designed to last. -

+
+ + + +
- {/* ─── Solutions ─── */} -
+ {/* ─── Featured Story: GIRVAK deep dive ─── */} +

- Services + Case Study

-

- What we build -

-
- -
-
- {useCases.map((useCase) => ( -
-

{useCase.title}

+
+
+

+ Türkiye Entrepreneurship +
+ Foundation +

+

+ Turkey’s largest entrepreneurship foundation manages + three national programs — including the Young Talent + Program, which receives 130,000+ applications annually. They + needed a system that could handle a five-stage evaluation + process, coordinate 100+ jury members, and automate thousands + of emails. +

+

+ We built the entire IT system on Airtable. +

+ + Read the full story → + +
+
+
+ {[ + { number: "130,000+", label: "applications / year" }, + { number: "100+", label: "jury evaluators" }, + { number: "5", label: "evaluation stages" }, + { number: "3", label: "national programs" }, + ].map((metric) => (
-
- ))} + key={metric.label} + className="p-5 rounded-lg bg-background border border-border" + > +

+ {metric.number} +

+

+ {metric.label} +

+
+ ))} +
- {/* ─── Numbers ─── */} -
+ {/* ─── More Stories: Visual story cards ─── */} +
-
- {[ - { number: "200+", label: "Automations built" }, - { number: "40,000+", label: "Tasks processed monthly" }, - { number: "150,000+", label: "Applications managed annually" }, - { number: "6+", label: "Organizations transformed" }, - ].map((stat) => ( -
-

- {stat.number} -

-

- {stat.label} -

-
+

+ More stories +

+ + +
+ + + +
+
+ + +
+

Trusted by

+ {customers.map((customer) => ( + {customer.name} ))}
- {/* ─── Customer Stories ─── */} -
+ {/* ─── Why Airtable: Real positioning ─── */} +
-

- Case Studies -

-

- Stories +

+ Why Airtable?

+

+ We chose to specialize in one platform and know it deeply. Here’s + why our clients choose it too. +

- -
- {customers.map((customer) => ( - - {customer.name} - - ))} +
+ } + title="One platform, not ten" + description="Our clients used to scatter data across Zapier, Google Sheets, and email. Now everything — automations, interfaces, dashboards — lives in Airtable." + /> + } + title="96% cheaper at scale" + description="We migrated a client's Zapier workflows to native Airtable automations. Same results, a fraction of the cost." + /> + } + title="Scales with your organization" + description="From 100 applicants to 130,000. Airtable grows with you — we make sure the system does too." + />
- -
- {featuredCustomers.slice(0, 3).map((customer, i) => ( - - -
-
-

- {customer.name} -

- - Read story → - -
-

- {customer.description} -

-
- -
- ))} -
- {/* ─── Testimonials ─── */} -
+ {/* ─── Testimonials: Real words from real people ─── */} +

@@ -227,19 +301,21 @@ export default function Page() {

- {/* ─── CTA ─── */} -
-
+ {/* ─── CTA: Specific, real, no-pressure ─── */} +
+

- Ready to work efficiently? + Every automation starts +
+ with a conversation.

- Let’s talk about how we can streamline your workflows. + 30 minutes. Free. No commitment.

- Book a 30-Minute Call + Book a Call
diff --git a/components/custom/impact-card.tsx b/components/custom/impact-card.tsx new file mode 100644 index 0000000..1c03a1d --- /dev/null +++ b/components/custom/impact-card.tsx @@ -0,0 +1,83 @@ +import { cn } from "@/lib/utils"; + +type Variant = "circles" | "grid" | "bars"; + +interface ImpactCardProps { + number: string; + label: string; + customer: string; + variant?: Variant; + className?: string; +} + +function CirclesDecoration() { + return ( +
+
+
+
+
+
+
+ ); +} + +function GridDecoration() { + return ( +
+ {Array.from({ length: 36 }).map((_, i) => ( +
+ ))} +
+ ); +} + +function BarsDecoration() { + return ( +
+
+
+
+
+
+
+
+
+ ); +} + +const decorations: Record = { + circles: CirclesDecoration, + grid: GridDecoration, + bars: BarsDecoration, +}; + +export default function ImpactCard({ + number, + label, + customer, + variant = "circles", + className, +}: ImpactCardProps) { + const Decoration = decorations[variant]; + + return ( +
+ +

+ {number} +

+

+ {label} +

+
+

{customer}

+
+
+ ); +} diff --git a/components/custom/position-card.tsx b/components/custom/position-card.tsx new file mode 100644 index 0000000..21ad7af --- /dev/null +++ b/components/custom/position-card.tsx @@ -0,0 +1,32 @@ +import { cn } from "@/lib/utils"; + +interface PositionCardProps { + icon: React.ReactNode; + title: string; + description: string; + className?: string; +} + +export default function PositionCard({ + icon, + title, + description, + className, +}: PositionCardProps) { + return ( +
+
+ {icon} +
+

{title}

+

+ {description} +

+
+ ); +} diff --git a/components/custom/story-card.tsx b/components/custom/story-card.tsx new file mode 100644 index 0000000..d8232dc --- /dev/null +++ b/components/custom/story-card.tsx @@ -0,0 +1,112 @@ +import Link from "next/link"; +import { cn } from "@/lib/utils"; + +type IllustrationVariant = "nodes" | "waves" | "blocks"; + +interface StoryCardProps { + title: string; + description: string; + href: string; + variant?: IllustrationVariant; + className?: string; +} + +function NodesIllustration() { + return ( + + + + + + + + + + + + + ); +} + +function WavesIllustration() { + return ( + + + + + + + + ); +} + +function BlocksIllustration() { + return ( + + + + + + + + + + + + ); +} + +const illustrations: Record = { + nodes: NodesIllustration, + waves: WavesIllustration, + blocks: BlocksIllustration, +}; + +export default function StoryCard({ + title, + description, + href, + variant = "nodes", + className, +}: StoryCardProps) { + const Illustration = illustrations[variant]; + + return ( + +
+
+ +
+
+

+ {title} +

+

+ {description} +

+ + Read story → + +
+
+ + ); +} From 48ac222788c1ba8247acfe877149285c05222db7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 27 Feb 2026 15:55:26 +0000 Subject: [PATCH 5/7] Add Aceternity-style cards, Services page, and Stories gallery New card components: - SpotlightCard: mouse-tracking radial gradient spotlight effect - TiltCard: 3D perspective tilt on hover with framer-motion spring - FocusCards: hover-to-focus gallery (others blur/dim) New pages: - /services: 6 services with SpotlightCards + real project references - /stories: FocusCards gallery of all customer stories with metrics Updates: - Homepage 'Why Airtable' now uses SpotlightCards - Navbar: added Services link, Stories now points to /stories - Sitemap: added /services and /stories Co-authored-by: Leo --- app/page.tsx | 90 +++++++++++----- app/services/page.tsx | 154 +++++++++++++++++++++++++++ app/sitemap.ts | 14 ++- app/stories/page.tsx | 70 ++++++++++++ components/custom/focus-cards.tsx | 73 +++++++++++++ components/custom/navbar.tsx | 10 +- components/custom/spotlight-card.tsx | 51 +++++++++ components/custom/tilt-card.tsx | 56 ++++++++++ 8 files changed, 485 insertions(+), 33 deletions(-) create mode 100644 app/services/page.tsx create mode 100644 app/stories/page.tsx create mode 100644 components/custom/focus-cards.tsx create mode 100644 components/custom/spotlight-card.tsx create mode 100644 components/custom/tilt-card.tsx diff --git a/app/page.tsx b/app/page.tsx index 55d3a65..ba753f6 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -5,7 +5,8 @@ import { FilloutButton } from "@/components/fillout"; import FadeIn from "@/components/custom/fade-in"; import ImpactCard from "@/components/custom/impact-card"; import StoryCard from "@/components/custom/story-card"; -import PositionCard from "@/components/custom/position-card"; +import SpotlightCard from "@/components/custom/spotlight-card"; +import TiltCard from "@/components/custom/tilt-card"; import { SiAirtable } from "react-icons/si"; function IconGrid() { @@ -205,24 +206,31 @@ export default function Page() { -
-

Trusted by

- {customers.map((customer) => ( - {customer.name} - ))} +
+ + View all customer stories → + +
+ {customers.map((customer) => ( + {customer.name} + ))} +
- {/* ─── Why Airtable: Real positioning ─── */} + {/* ─── Why Airtable: Real positioning with SpotlightCards ─── */}
@@ -236,21 +244,45 @@ export default function Page() {
- } - title="One platform, not ten" - description="Our clients used to scatter data across Zapier, Google Sheets, and email. Now everything — automations, interfaces, dashboards — lives in Airtable." - /> - } - title="96% cheaper at scale" - description="We migrated a client's Zapier workflows to native Airtable automations. Same results, a fraction of the cost." - /> - } - title="Scales with your organization" - description="From 100 applicants to 130,000. Airtable grows with you — we make sure the system does too." - /> + +
+
+ +
+

One platform, not ten

+

+ Our clients used to scatter data across Zapier, Google Sheets, + and email. Now everything — automations, interfaces, + dashboards — lives in Airtable. +

+
+
+ +
+
+ +
+

96% cheaper at scale

+

+ We migrated a client’s Zapier workflows to native + Airtable automations. Same results, a fraction of the cost. +

+
+
+ +
+
+ +
+

+ Scales with your organization +

+

+ From 100 applicants to 130,000. Airtable grows with + you — we make sure the system does too. +

+
+
diff --git a/app/services/page.tsx b/app/services/page.tsx new file mode 100644 index 0000000..f9f2844 --- /dev/null +++ b/app/services/page.tsx @@ -0,0 +1,154 @@ +import information from "@/information.json"; +import { Metadata } from "next"; +import { FilloutButton } from "@/components/fillout"; +import FadeIn from "@/components/custom/fade-in"; +import SpotlightCard from "@/components/custom/spotlight-card"; + +export async function generateMetadata(): Promise { + const title = "Services"; + const description = + "Airtable consulting, automation, data migration, integrations, training, and ongoing support."; + + return { + title, + description, + openGraph: { + title, + description, + url: information.website, + type: "website", + images: [ + { + url: `${information.website}/api/og?title=${title}`, + alt: `${information.company} Logo`, + }, + ], + }, + }; +} + +const services = [ + { + icon: ( + + + + ), + title: "Base Design & Implementation", + description: + "We architect your Airtable system from scratch. Tables, views, relationships, and permissions — designed for how your organization actually works, not how a template thinks it should.", + }, + { + icon: ( + + + + ), + title: "Workflow Automation", + description: + "Native Airtable automations and scripts that replace manual processes. From email triggers to complex multi-step workflows processing thousands of tasks daily — like the 200+ automations we run for PublicSquare.", + }, + { + icon: ( + + + + ), + title: "Data Migration", + description: + "Moving from spreadsheets, Zapier, or legacy tools to Airtable. We've cut client automation costs by 96% migrating from Zapier to native Airtable. Clean data, zero downtime.", + }, + { + icon: ( + + + + ), + title: "Custom Integrations", + description: + "Connecting Airtable with Brevo, Slack, Zoom, payment systems, and custom APIs. One system that talks to everything else — like the Brevo email integration we built for GIRVAK handling 130,000+ applications.", + }, + { + icon: ( + + + + ), + title: "Training & Education", + description: + "Hands-on sessions for your team. We don't just build — we make sure you understand the system and can manage it yourselves. We also run a free Airtable course on YouTube.", + }, + { + icon: ( + + + + ), + title: "Ongoing Support", + description: + "We stay. Monitoring, updates, and optimization as your organization grows. Your system evolves with you — not a launch-and-leave engagement.", + }, +]; + +export default function ServicesPage() { + return ( + <> +
+
+ +

+ What we do +

+

+ We build Airtable systems +
+ that run themselves. +

+

+ From architecture to automation to ongoing support. Every service + is grounded in real projects across three continents. +

+
+ + +
+ {services.map((service) => ( + +
+
+ {service.icon} +
+

{service.title}

+

+ {service.description} +

+
+
+ ))} +
+
+
+
+ +
+
+ +

+ Not sure where to start? +

+

+ Most of our projects begin with a free 30-minute call. We’ll + listen, ask questions, and tell you honestly if Airtable is the + right fit. +

+
+ + Book a Free Call + +
+
+
+
+ + ); +} diff --git a/app/sitemap.ts b/app/sitemap.ts index 759df45..c3cb636 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -14,15 +14,25 @@ export default function sitemap(): MetadataRoute.Sitemap { priority: 1.0, }, { - url: "https://useefficiently.com/about", + url: "https://useefficiently.com/services", changeFrequency: "monthly", priority: 0.9, }, { - url: "https://useefficiently.com/schedule-meeting", + url: "https://useefficiently.com/stories", changeFrequency: "monthly", priority: 0.9, }, + { + url: "https://useefficiently.com/about", + changeFrequency: "monthly", + priority: 0.8, + }, + { + url: "https://useefficiently.com/schedule-meeting", + changeFrequency: "monthly", + priority: 0.8, + }, ...customerSitemap, ]; } diff --git a/app/stories/page.tsx b/app/stories/page.tsx new file mode 100644 index 0000000..32f827a --- /dev/null +++ b/app/stories/page.tsx @@ -0,0 +1,70 @@ +import information from "@/information.json"; +import { Metadata } from "next"; +import { customers } from "@/use"; +import FadeIn from "@/components/custom/fade-in"; +import FocusCards from "@/components/custom/focus-cards"; + +export async function generateMetadata(): Promise { + const title = "Customer Stories"; + const description = + "Real projects, real numbers. See how organizations across three continents run on Airtable systems we built."; + + return { + title, + description, + openGraph: { + title, + description, + url: information.website, + type: "website", + images: [ + { + url: `${information.website}/api/og?title=${title}`, + alt: `${information.company} Logo`, + }, + ], + }, + }; +} + +const storyMetrics: Record = { + publicsquare: { metric: "200+", metricLabel: "automations" }, + girvak: { metric: "130,000+", metricLabel: "applications / year" }, + yetgen: { metric: "10,000+", metricLabel: "applicants / cohort" }, + oua: { metric: "50,000+", metricLabel: "applications / year" }, +}; + +export default function StoriesPage() { + const items = customers.map((c) => ({ + title: c.name, + description: c.description, + image: c.logo, + href: `/${c.slug}`, + ...storyMetrics[c.slug], + })); + + return ( +
+
+ +

+ Case Studies +

+

+ Customer Stories +

+

+ Real projects. Real numbers. Organizations across three continents + running on the Airtable systems we built. +

+
+ + +
+ +
+
+
+
+ ); +} diff --git a/components/custom/focus-cards.tsx b/components/custom/focus-cards.tsx new file mode 100644 index 0000000..236c2d1 --- /dev/null +++ b/components/custom/focus-cards.tsx @@ -0,0 +1,73 @@ +"use client"; + +import { useState } from "react"; +import Image from "next/image"; +import Link from "next/link"; +import { cn } from "@/lib/utils"; + +interface FocusCardItem { + title: string; + description: string; + image: string; + href: string; + metric?: string; + metricLabel?: string; +} + +interface FocusCardsProps { + items: FocusCardItem[]; + className?: string; +} + +export default function FocusCards({ items, className }: FocusCardsProps) { + const [hovered, setHovered] = useState(null); + + return ( +
setHovered(null)} + > + {items.map((item, i) => ( + setHovered(i)} + className={cn( + "group relative overflow-hidden rounded-xl border border-border bg-card transition-all duration-300", + hovered !== null && hovered !== i && "blur-[2px] opacity-40 scale-[0.98]", + hovered === i && "shadow-xl scale-[1.02]" + )} + > +
+ {item.title} +
+
+

{item.title}

+ {item.description && ( +

+ {item.description} +

+ )} + {item.metric && ( +
+ {item.metric} + + {item.metricLabel} + +
+ )} +
+ + ))} +
+ ); +} diff --git a/components/custom/navbar.tsx b/components/custom/navbar.tsx index 71f9e00..3ee2fc1 100644 --- a/components/custom/navbar.tsx +++ b/components/custom/navbar.tsx @@ -15,14 +15,20 @@ export default function Navbar() {
+ Services + + Stories About diff --git a/components/custom/spotlight-card.tsx b/components/custom/spotlight-card.tsx new file mode 100644 index 0000000..0a14f55 --- /dev/null +++ b/components/custom/spotlight-card.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useRef, useState, type MouseEvent } from "react"; +import { cn } from "@/lib/utils"; + +interface SpotlightCardProps { + children: React.ReactNode; + className?: string; + spotlightColor?: string; + spotlightSize?: number; +} + +export default function SpotlightCard({ + children, + className, + spotlightColor = "rgba(120,113,108,0.08)", + spotlightSize = 400, +}: SpotlightCardProps) { + const ref = useRef(null); + const [position, setPosition] = useState({ x: 0, y: 0 }); + const [isHovered, setIsHovered] = useState(false); + + function handleMouseMove(e: MouseEvent) { + const rect = ref.current?.getBoundingClientRect(); + if (!rect) return; + setPosition({ x: e.clientX - rect.left, y: e.clientY - rect.top }); + } + + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + className={cn( + "relative overflow-hidden rounded-xl border border-border bg-card transition-shadow duration-300", + isHovered && "shadow-lg", + className + )} + > +
+
{children}
+
+ ); +} diff --git a/components/custom/tilt-card.tsx b/components/custom/tilt-card.tsx new file mode 100644 index 0000000..3b46ecd --- /dev/null +++ b/components/custom/tilt-card.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { useRef, useState, type MouseEvent } from "react"; +import { motion } from "framer-motion"; +import { cn } from "@/lib/utils"; + +interface TiltCardProps { + children: React.ReactNode; + className?: string; + tiltDegree?: number; +} + +export default function TiltCard({ + children, + className, + tiltDegree = 8, +}: TiltCardProps) { + const ref = useRef(null); + const [rotateX, setRotateX] = useState(0); + const [rotateY, setRotateY] = useState(0); + const [isHovered, setIsHovered] = useState(false); + + function handleMouseMove(e: MouseEvent) { + const rect = ref.current?.getBoundingClientRect(); + if (!rect) return; + const x = (e.clientX - rect.left - rect.width / 2) / (rect.width / 2); + const y = (e.clientY - rect.top - rect.height / 2) / (rect.height / 2); + setRotateX(-y * tiltDegree); + setRotateY(x * tiltDegree); + } + + function handleMouseLeave() { + setRotateX(0); + setRotateY(0); + setIsHovered(false); + } + + return ( + setIsHovered(true)} + onMouseLeave={handleMouseLeave} + animate={{ rotateX, rotateY }} + transition={{ type: "spring", stiffness: 260, damping: 20 }} + style={{ perspective: 800, transformStyle: "preserve-3d" }} + className={cn( + "rounded-xl border border-border bg-card overflow-hidden transition-shadow duration-300", + isHovered && "shadow-xl", + className + )} + > +
{children}
+
+ ); +} From 74517b200b119505dc25324be85b68d1c48d7279 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 27 Feb 2026 20:26:52 +0000 Subject: [PATCH 6/7] =?UTF-8?q?Honest,=20grounded=20redesign=20=E2=80=94?= =?UTF-8?q?=20remove=20Aceternity=20effects,=20fix=20tone?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tone shift: trustworthy, simple, no big promises, no uppercase labels. Hero: 'We build Airtable systems for organizations that need things to actually work.' — tells the team's story alongside customer results. - Remove SpotlightCard, TiltCard, FocusCards (Aceternity effects) - Rewrite services: real countries (UK, Canada, Australia, Turkey, USA), real examples baked in, not generic service names - Stories page: clean list layout with logos and metrics - Footer: UseEfficiently LLC (USA-based), remove Istanbul reference - Remove all uppercase text (tracking-wide uppercase) across every page - CTA: 'No pitch, no pressure. We'll listen and tell you honestly whether Airtable is the right fit.' - Why Airtable: conversational tone, real client examples - [slug] back links point to /stories Co-authored-by: Leo --- app/[slug]/page.tsx | 4 +- app/page.tsx | 231 +++++++++------------------ app/services/page.tsx | 101 ++++-------- app/stories/page.tsx | 74 ++++++--- components/custom/focus-cards.tsx | 73 --------- components/custom/site-footer.tsx | 6 +- components/custom/spotlight-card.tsx | 51 ------ components/custom/tilt-card.tsx | 56 ------- 8 files changed, 163 insertions(+), 433 deletions(-) delete mode 100644 components/custom/focus-cards.tsx delete mode 100644 components/custom/spotlight-card.tsx delete mode 100644 components/custom/tilt-card.tsx diff --git a/app/[slug]/page.tsx b/app/[slug]/page.tsx index 7462e6f..3c52807 100644 --- a/app/[slug]/page.tsx +++ b/app/[slug]/page.tsx @@ -50,7 +50,7 @@ export default function CustomerStory({ params }: Props) {
← Back to stories @@ -79,7 +79,7 @@ export default function CustomerStory({ params }: Props) {
← Back to all stories diff --git a/app/page.tsx b/app/page.tsx index ba753f6..c28738b 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -5,90 +5,42 @@ import { FilloutButton } from "@/components/fillout"; import FadeIn from "@/components/custom/fade-in"; import ImpactCard from "@/components/custom/impact-card"; import StoryCard from "@/components/custom/story-card"; -import SpotlightCard from "@/components/custom/spotlight-card"; -import TiltCard from "@/components/custom/tilt-card"; -import { SiAirtable } from "react-icons/si"; - -function IconGrid() { - return ( - - - - ); -} - -function IconTrendDown() { - return ( - - - - ); -} - -function IconScale() { - return ( - - - - ); -} export default function Page() { - const girvak = customers.find((c) => c.slug === "girvak"); - return ( <> - {/* ─── Hero: Customer-first opening ─── */} + {/* ─── Hero ─── */}
-
-

- Airtable Services Partner · Istanbul -

-

- 130,000 applications. +
+

+ We build Airtable systems
- Zero manual steps. + for organizations that need +
+ things to actually work.

-

- We built the automation system behind Turkey’s largest young - talent program. Now it runs itself. +

+ We’re a small team. We’ve worked with clients in Turkey, + Australia, Canada, the UK, and the USA. Some needed to automate 200 + workflows. Others needed to process 130,000 applications a year. We + helped, and we stuck around to make sure it kept working.

- Book a Free Call + Book a free call - See the stories ↓ + See what we’ve done ↓

- {/* ─── Impact: Real numbers from real clients ─── */} -
+ {/* ─── Numbers ─── */} +
@@ -107,7 +59,7 @@ export default function Page() {
@@ -115,36 +67,31 @@ export default function Page() {
- {/* ─── Featured Story: GIRVAK deep dive ─── */} -
+ {/* ─── Featured story ─── */} +
-

- Case Study -

- Türkiye Entrepreneurship -
- Foundation + Türkiye Entrepreneurship Foundation

- Turkey’s largest entrepreneurship foundation manages - three national programs — including the Young Talent - Program, which receives 130,000+ applications annually. They - needed a system that could handle a five-stage evaluation - process, coordinate 100+ jury members, and automate thousands - of emails. + Turkey’s largest entrepreneurship foundation runs three + national programs, including the Young Talent Program — 130,000+ + applications a year, a five-stage evaluation with 100+ jury + members, and thousands of automated emails. They needed a system + that could handle all of it.

-

- We built the entire IT system on Airtable. +

+ We built the whole thing on Airtable. It’s been running + since.

- Read the full story → + Read the full story →
@@ -174,7 +121,7 @@ export default function Page() {
- {/* ─── More Stories: Visual story cards ─── */} + {/* ─── More stories ─── */}
@@ -186,19 +133,19 @@ export default function Page() {
@@ -206,97 +153,71 @@ export default function Page() { -
+
- View all customer stories → + View all stories → -
- {customers.map((customer) => ( - {customer.name} - ))} -
- {/* ─── Why Airtable: Real positioning with SpotlightCards ─── */} -
+ {/* ─── Why Airtable ─── */} +

Why Airtable?

-

- We chose to specialize in one platform and know it deeply. Here’s - why our clients choose it too. +

+ We picked one tool and learned it deeply. Airtable is flexible + enough to replace a dozen scattered apps, and powerful enough to + handle real scale. That’s why we built our whole practice + around it.

- -
-
- -
-

One platform, not ten

-

- Our clients used to scatter data across Zapier, Google Sheets, - and email. Now everything — automations, interfaces, - dashboards — lives in Airtable. -

-
-
- -
-
- -
-

96% cheaper at scale

-

- We migrated a client’s Zapier workflows to native - Airtable automations. Same results, a fraction of the cost. -

-
-
- -
-
- -
-

- Scales with your organization -

-

- From 100 applicants to 130,000. Airtable grows with - you — we make sure the system does too. -

-
-
+
+

One place for everything

+

+ Most of our clients came to us with data in five different + tools. We moved it all into Airtable — automations, interfaces, + dashboards, and the data itself. +

+
+
+

Much cheaper at scale

+

+ One client was spending a lot on Zapier. We migrated their + workflows to native Airtable automations — same results, 96% + less cost. +

+
+
+

It actually scales

+

+ We’ve seen Airtable handle 100 applicants and 130,000 + applicants in the same system. It grows with you — we make sure + the architecture supports it. +

+
- {/* ─── Testimonials: Real words from real people ─── */} + {/* ─── Testimonials ─── */}

- What our clients say + What people say about working with us

-
{testimonials.map((testimonial, i) => ( @@ -333,21 +254,21 @@ export default function Page() {
- {/* ─── CTA: Specific, real, no-pressure ─── */} + {/* ─── CTA ─── */}

- Every automation starts -
- with a conversation. + Want to talk?

-

- 30 minutes. Free. No commitment. +

+ We do a free 30-minute call. No pitch, no pressure. We’ll + listen to what you need and tell you honestly whether Airtable is + the right fit.

- Book a Call + Book a call
diff --git a/app/services/page.tsx b/app/services/page.tsx index f9f2844..bc47caf 100644 --- a/app/services/page.tsx +++ b/app/services/page.tsx @@ -2,12 +2,11 @@ import information from "@/information.json"; import { Metadata } from "next"; import { FilloutButton } from "@/components/fillout"; import FadeIn from "@/components/custom/fade-in"; -import SpotlightCard from "@/components/custom/spotlight-card"; export async function generateMetadata(): Promise { const title = "Services"; const description = - "Airtable consulting, automation, data migration, integrations, training, and ongoing support."; + "Airtable consulting, automation, data migration, integrations, training, and ongoing support for clients in the UK, Canada, Australia, Turkey, and the USA."; return { title, @@ -29,64 +28,34 @@ export async function generateMetadata(): Promise { const services = [ { - icon: ( - - - - ), - title: "Base Design & Implementation", + title: "Base design and implementation", description: - "We architect your Airtable system from scratch. Tables, views, relationships, and permissions — designed for how your organization actually works, not how a template thinks it should.", + "We set up your Airtable from scratch — tables, views, relationships, permissions. Not from a template. Designed around how your organization actually works.", }, { - icon: ( - - - - ), - title: "Workflow Automation", + title: "Workflow automation", description: - "Native Airtable automations and scripts that replace manual processes. From email triggers to complex multi-step workflows processing thousands of tasks daily — like the 200+ automations we run for PublicSquare.", + "Airtable automations and scripts that replace the manual stuff. We've built 200+ automations for a single client in Australia that process 40,000 tasks a month.", }, { - icon: ( - - - - ), - title: "Data Migration", + title: "Data migration", description: - "Moving from spreadsheets, Zapier, or legacy tools to Airtable. We've cut client automation costs by 96% migrating from Zapier to native Airtable. Clean data, zero downtime.", + "Moving from spreadsheets, Zapier, or whatever you're using now. One client was paying a lot for Zapier — we moved them to native Airtable and cut their costs by 96%.", }, { - icon: ( - - - - ), - title: "Custom Integrations", + title: "Custom integrations", description: - "Connecting Airtable with Brevo, Slack, Zoom, payment systems, and custom APIs. One system that talks to everything else — like the Brevo email integration we built for GIRVAK handling 130,000+ applications.", + "Connecting Airtable with tools like Brevo, Slack, Zoom, or your own APIs. For a foundation in Turkey, we built a Brevo integration that sends thousands of automated emails during their application season.", }, { - icon: ( - - - - ), - title: "Training & Education", + title: "Training", description: - "Hands-on sessions for your team. We don't just build — we make sure you understand the system and can manage it yourselves. We also run a free Airtable course on YouTube.", + "We teach your team how the system works so you're not dependent on us. We also have a free Airtable course on YouTube if you want to learn on your own first.", }, { - icon: ( - - - - ), - title: "Ongoing Support", + title: "Ongoing support", description: - "We stay. Monitoring, updates, and optimization as your organization grows. Your system evolves with you — not a launch-and-leave engagement.", + "We don't disappear after launch. We monitor, update, and improve the system as your needs change. Most of our client relationships are long-term.", }, ]; @@ -96,34 +65,28 @@ export default function ServicesPage() {
-

- What we do -

- We build Airtable systems -
- that run themselves. + What we do

- From architecture to automation to ongoing support. Every service - is grounded in real projects across three continents. + We work with Airtable. That’s our focus. We’ve been + building on it for clients in the UK, Canada, Australia, Turkey, + and the USA — from small startups to national foundations.

-
+
{services.map((service) => ( - -
-
- {service.icon} -
-

{service.title}

-

- {service.description} -

-
-
+
+

{service.title}

+

+ {service.description} +

+
))}
@@ -136,14 +99,14 @@ export default function ServicesPage() {

Not sure where to start?

-

- Most of our projects begin with a free 30-minute call. We’ll - listen, ask questions, and tell you honestly if Airtable is the - right fit. +

+ Most projects start with a 30-minute call. We’ll listen, ask + questions, and tell you honestly if Airtable makes sense for your + situation.

- Book a Free Call + Book a free call
diff --git a/app/stories/page.tsx b/app/stories/page.tsx index 32f827a..bdd1794 100644 --- a/app/stories/page.tsx +++ b/app/stories/page.tsx @@ -1,13 +1,14 @@ +import Image from "next/image"; +import Link from "next/link"; import information from "@/information.json"; import { Metadata } from "next"; import { customers } from "@/use"; import FadeIn from "@/components/custom/fade-in"; -import FocusCards from "@/components/custom/focus-cards"; export async function generateMetadata(): Promise { - const title = "Customer Stories"; + const title = "Stories"; const description = - "Real projects, real numbers. See how organizations across three continents run on Airtable systems we built."; + "Real projects from real organizations in Turkey, Australia, Canada, and beyond."; return { title, @@ -27,41 +28,66 @@ export async function generateMetadata(): Promise { }; } -const storyMetrics: Record = { - publicsquare: { metric: "200+", metricLabel: "automations" }, - girvak: { metric: "130,000+", metricLabel: "applications / year" }, - yetgen: { metric: "10,000+", metricLabel: "applicants / cohort" }, - oua: { metric: "50,000+", metricLabel: "applications / year" }, +const metrics: Record = { + publicsquare: "200+ automations", + girvak: "130,000+ applications / year", + yetgen: "10,000+ applicants / cohort", + oua: "50,000+ applications / year", }; export default function StoriesPage() { - const items = customers.map((c) => ({ - title: c.name, - description: c.description, - image: c.logo, - href: `/${c.slug}`, - ...storyMetrics[c.slug], - })); - return (
-

- Case Studies -

- Customer Stories + Stories

- Real projects. Real numbers. Organizations across three continents - running on the Airtable systems we built. + These are organizations we’ve worked with. Real projects, real + numbers.

-
- +
+ {customers.map((customer) => ( + +
+
+ {customer.name} +
+
+

+ {customer.name} +

+ {customer.description && ( +

+ {customer.description} +

+ )} +
+
+ {metrics[customer.slug] && ( +

+ {metrics[customer.slug]} +

+ )} +
+
+ + ))} +
diff --git a/components/custom/focus-cards.tsx b/components/custom/focus-cards.tsx deleted file mode 100644 index 236c2d1..0000000 --- a/components/custom/focus-cards.tsx +++ /dev/null @@ -1,73 +0,0 @@ -"use client"; - -import { useState } from "react"; -import Image from "next/image"; -import Link from "next/link"; -import { cn } from "@/lib/utils"; - -interface FocusCardItem { - title: string; - description: string; - image: string; - href: string; - metric?: string; - metricLabel?: string; -} - -interface FocusCardsProps { - items: FocusCardItem[]; - className?: string; -} - -export default function FocusCards({ items, className }: FocusCardsProps) { - const [hovered, setHovered] = useState(null); - - return ( -
setHovered(null)} - > - {items.map((item, i) => ( - setHovered(i)} - className={cn( - "group relative overflow-hidden rounded-xl border border-border bg-card transition-all duration-300", - hovered !== null && hovered !== i && "blur-[2px] opacity-40 scale-[0.98]", - hovered === i && "shadow-xl scale-[1.02]" - )} - > -
- {item.title} -
-
-

{item.title}

- {item.description && ( -

- {item.description} -

- )} - {item.metric && ( -
- {item.metric} - - {item.metricLabel} - -
- )} -
- - ))} -
- ); -} diff --git a/components/custom/site-footer.tsx b/components/custom/site-footer.tsx index e6c6030..203b2f7 100644 --- a/components/custom/site-footer.tsx +++ b/components/custom/site-footer.tsx @@ -7,9 +7,9 @@ export default function SiteFooter() {
-

UseEfficiently

+

UseEfficiently LLC

- Accredited Airtable Services Partner + Airtable Services Partner

@@ -44,7 +44,7 @@ export default function SiteFooter() {
-

© {new Date().getFullYear()} UseEfficiently

+

© {new Date().getFullYear()} UseEfficiently LLC

diff --git a/components/custom/spotlight-card.tsx b/components/custom/spotlight-card.tsx deleted file mode 100644 index 0a14f55..0000000 --- a/components/custom/spotlight-card.tsx +++ /dev/null @@ -1,51 +0,0 @@ -"use client"; - -import { useRef, useState, type MouseEvent } from "react"; -import { cn } from "@/lib/utils"; - -interface SpotlightCardProps { - children: React.ReactNode; - className?: string; - spotlightColor?: string; - spotlightSize?: number; -} - -export default function SpotlightCard({ - children, - className, - spotlightColor = "rgba(120,113,108,0.08)", - spotlightSize = 400, -}: SpotlightCardProps) { - const ref = useRef(null); - const [position, setPosition] = useState({ x: 0, y: 0 }); - const [isHovered, setIsHovered] = useState(false); - - function handleMouseMove(e: MouseEvent) { - const rect = ref.current?.getBoundingClientRect(); - if (!rect) return; - setPosition({ x: e.clientX - rect.left, y: e.clientY - rect.top }); - } - - return ( -
setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - className={cn( - "relative overflow-hidden rounded-xl border border-border bg-card transition-shadow duration-300", - isHovered && "shadow-lg", - className - )} - > -
-
{children}
-
- ); -} diff --git a/components/custom/tilt-card.tsx b/components/custom/tilt-card.tsx deleted file mode 100644 index 3b46ecd..0000000 --- a/components/custom/tilt-card.tsx +++ /dev/null @@ -1,56 +0,0 @@ -"use client"; - -import { useRef, useState, type MouseEvent } from "react"; -import { motion } from "framer-motion"; -import { cn } from "@/lib/utils"; - -interface TiltCardProps { - children: React.ReactNode; - className?: string; - tiltDegree?: number; -} - -export default function TiltCard({ - children, - className, - tiltDegree = 8, -}: TiltCardProps) { - const ref = useRef(null); - const [rotateX, setRotateX] = useState(0); - const [rotateY, setRotateY] = useState(0); - const [isHovered, setIsHovered] = useState(false); - - function handleMouseMove(e: MouseEvent) { - const rect = ref.current?.getBoundingClientRect(); - if (!rect) return; - const x = (e.clientX - rect.left - rect.width / 2) / (rect.width / 2); - const y = (e.clientY - rect.top - rect.height / 2) / (rect.height / 2); - setRotateX(-y * tiltDegree); - setRotateY(x * tiltDegree); - } - - function handleMouseLeave() { - setRotateX(0); - setRotateY(0); - setIsHovered(false); - } - - return ( - setIsHovered(true)} - onMouseLeave={handleMouseLeave} - animate={{ rotateX, rotateY }} - transition={{ type: "spring", stiffness: 260, damping: 20 }} - style={{ perspective: 800, transformStyle: "preserve-3d" }} - className={cn( - "rounded-xl border border-border bg-card overflow-hidden transition-shadow duration-300", - isHovered && "shadow-xl", - className - )} - > -
{children}
-
- ); -} From 404841c8adad94c70203e49af687c328eb8df54b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 27 Feb 2026 20:54:05 +0000 Subject: [PATCH 7/7] Fix unfinished parts: repeating content, empty pages, missing bio MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - [slug] pages: remove separate description paragraph that duplicated the opening of detail HTML (fixes /publicsquare, /girvak, /yetgen, /oua) - Empty story pages (/oihs, /gkp): show 'Story coming soon' message with a CTA to book a call instead of blank page - Stories list: split customers with/without content — empty ones show as grayed out with 'Story coming soon' instead of dead links - About page: tighten spacing for team members without a bio (Yunus) Co-authored-by: Leo --- app/[slug]/page.tsx | 72 ++++++++++++++++++++++++++++++++++---------- app/about/page.tsx | 6 ++-- app/stories/page.tsx | 35 +++++++++++++++++++-- 3 files changed, 92 insertions(+), 21 deletions(-) diff --git a/app/[slug]/page.tsx b/app/[slug]/page.tsx index 3c52807..4390fa4 100644 --- a/app/[slug]/page.tsx +++ b/app/[slug]/page.tsx @@ -3,6 +3,7 @@ import information from "@/information.json"; import Link from "next/link"; import { customers } from "@/use"; import { redirect } from "next/navigation"; +import { FilloutButton } from "@/components/fillout"; type Props = { params: { @@ -46,6 +47,53 @@ export default function CustomerStory({ params }: Props) { const customer = customers.find((c) => c.slug === params.slug); if (!customer) redirect("/"); + const hasContent = customer.detail && customer.detail.trim().length > 0; + + if (!hasContent) { + return ( +
+
+ + ← Back to stories + + +

+ {customer.name} +

+ +

+ We worked with {customer.name} on their Airtable system. Full story + coming soon. +

+ +
+

Want to hear more about this project?

+

+ We’re happy to walk you through it on a call. +

+
+ + Book a call + +
+
+ +
+ + ← Back to all stories + +
+
+
+ ); + } + return (
@@ -60,22 +108,14 @@ export default function CustomerStory({ params }: Props) { {customer.name} - {customer.description && ( -

- {customer.description} -

- )} - - {customer.detail && ( -
- )} +
{member.title} · {member.address}

- {member.quote && ( + {member.quote ? (

{member.quote}

- )} + ) : null} LinkedIn → diff --git a/app/stories/page.tsx b/app/stories/page.tsx index bdd1794..b127f2c 100644 --- a/app/stories/page.tsx +++ b/app/stories/page.tsx @@ -36,6 +36,13 @@ const metrics: Record = { }; export default function StoriesPage() { + const withContent = customers.filter( + (c) => c.detail && c.detail.trim().length > 0 + ); + const withoutContent = customers.filter( + (c) => !c.detail || c.detail.trim().length === 0 + ); + return (
@@ -51,10 +58,10 @@ export default function StoriesPage() {
- {customers.map((customer) => ( + {withContent.map((customer) => (
@@ -87,6 +94,30 @@ export default function StoriesPage() {
))} + + {withoutContent.length > 0 && ( + <> + {withoutContent.map((customer) => ( +
+
+ {customer.name} +
+
+

+ {customer.name} +

+
+

Story coming soon

+
+ ))} + + )}