diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..0bef971 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,38 @@ +# 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` | + +### 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. diff --git a/app/[slug]/page.tsx b/app/[slug]/page.tsx index dd179b6..4390fa4 100644 --- a/app/[slug]/page.tsx +++ b/app/[slug]/page.tsx @@ -2,7 +2,8 @@ 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"; +import { FilloutButton } from "@/components/fillout"; type Props = { params: { @@ -13,55 +14,118 @@ 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("/"); - return ( - <> -
- + 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 ( +
+
+ + ← Back to stories + + +

+ {customer.name} +

+ +
-
-
- +
+ + ← Back to all stories + +
- +
); } diff --git a/app/about/page.tsx b/app/about/page.tsx index ae04237..4407565 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}
-
-