Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 72 additions & 126 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,138 +1,84 @@
# AgentPage

Public profile builder for real estate agents. Agents get a branded page at `/:username` with listings, reviews, lead capture, and widgets. (Linktree alternative)
Public profile builder for real estate agents. Each agent gets a branded page at `/:username` with listings, reviews, lead capture, and widgets — a Linktree alternative built for real estate.

## Tech Stack

| Layer | Technology |
| -------------- | ------------------------------------------------------- |
| Framework | Next.js 15 App Router |
| Language | TypeScript |
| Database | PostgreSQL + Prisma 7 (`prisma db push`, no migrations) |
| Auth | Lucia v3 sessions + Google OAuth (Arctic) |
| Payments | Stripe — plans `solo` / `pro` |
| Storage | AWS S3 + CloudFront |
| Rate Limiting | AWS DynamoDB |
| Email | Resend |
| i18n | next-intl (`en`, `fr`) |
| UI dashboard | Tailwind CSS 4 + DaisyUI |
| UI public page | Custom theme system — no DaisyUI |

## Environment Variables

```env
APP_URL= # e.g. https://yourdomain.com
DATABASE_URL=
AWS_REGION=
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
BUCKET_NAME= # S3 bucket for uploads
CLOUDFRONT_URL= # Private (server-side) CDN URL
NEXT_PUBLIC_CLOUDFRONT_URL= # Public CDN URL for <img> tags
RATE_LIMITS_DYNAMODB_TABLE=
RESEND_API_KEY=
SENDER_EMAIL=
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
STRIPE_PRICE_SOLO_MONTHLY= # Stripe Price ID
STRIPE_PRICE_SOLO_ANNUAL=
STRIPE_PRICE_PRO_MONTHLY=
STRIPE_PRICE_PRO_ANNUAL=
```

## Setup
## Getting Started

```bash
bun install && cp .env.example .env.local # fill in env vars
bunx prisma db push # sync schema to DB
bun install
bunx prisma db push
bun dev
```

## Project Structure

```
src/
app/
[locale]/
(dashboard)/ # Authenticated dashboard routes
(public)/[username]/ # Public agent page
api/
auth/ # register, login, logout, google oauth
dashboard/ # profile, links, reviews, listings, widgets, themes
stripe/ # checkout, portal, webhook
lib/
auth.ts # getUser() → { user, session } | { user: null }
db.ts # Prisma client singleton
env.ts # Type-safe env via @t3-oss/env-nextjs
subscription.ts # isPro(), getActivePlan(), hasActiveSubscription()
themes.ts # getTheme() → ThemeConfig
components/
ui/pro-gate.tsx # Locks UI behind pro plan
generated/prisma/ # Prisma client output
locales/
en.json # All translation keys
fr.json
```

## Key Patterns

### Auth

`getUser()` from `@/lib/auth` returns `{ user, session }` or `{ user: null, session: null }`. Use in server components and API routes. Sessions stored in `Session` table; cookies set via Lucia.

### Subscriptions & Pro Gating

Plans: `solo` (paid base) and `pro` (paid advanced). Status values: `active`, `trialing`, `past_due`, `canceled`.

```ts
import { isPro } from "@/lib/subscription";
const pro = await isPro(userId); // true if active/trialing pro subscription
```

Use `<ProGate>` wrapper for client components. Stripe customer ID is created eagerly at registration so the billing portal is always accessible.

### Theme System (public page only)

Dashboard uses DaisyUI. The public `/:username` page uses a custom theme — **never use DaisyUI classes here**.

```ts
import { getTheme } from "@/lib/themes";
const theme = getTheme(
user.theme,
user.accentColor,
user.buttonRadius,
user.fontFamily,
user.fontSize
);
// theme.bg, theme.cardBg, theme.cardText, theme.border, theme.muted → Tailwind classes
// theme.accentHex, theme.accentTextColor → hex strings (use inline style)
// theme.buttonRadius → Tailwind class (e.g. "rounded-lg")
// theme.fontFamily, theme.fontSize → CSS string / Tailwind class
```

### i18n

- Server components: `const t = await getTranslations("namespace")`
- Client components: `const t = useTranslations("namespace")`
- All keys live in `locales/en.json` and `locales/fr.json`
- Namespaces: `common`, `pages.dashboard`, `pages.dashboard-listings`, `pages.agent-page`, etc.

### Image Uploads

Upload via `POST /api/dashboard/images` → returns CloudFront URL. Use `NEXT_PUBLIC_CLOUDFRONT_URL` in `<Image>` src.

### Stripe Webhook

Events at `POST /api/stripe/webhook`. On `checkout.session.completed`: cancels existing active/trialing subscriptions before creating the new one. Syncs status on `updated`, `deleted`, and `invoice.payment_*`.

## Database Models

`User` · `Session` · `Token` · `Subscription` · `Link` · `Review` · `Analytics` · `Lead` · `Listing`
## Tech Stack

Schema managed with `prisma db push` (no migrations). Client output in `src/generated/prisma`.
| | |
| ------------- | ------------------------------------------- |
| **Framework** | Next.js 16 · React 19 · TypeScript 5.9 |
| **Database** | PostgreSQL · Prisma 7 |
| **Auth** | Lucia v3 · Google OAuth (Arctic) |
| **Payments** | Stripe 19 |
| **Storage** | AWS S3 · CloudFront |
| **Email** | Resend |
| **UI** | Tailwind CSS 4 · DaisyUI 5 |
| **i18n** | next-intl 4 (EN / FR) |
| **Infra** | Pulumi (IaC) · AWS DynamoDB (rate limiting) |

## Features

**Public agent page**

- Customizable profile — photo, bio, brokerage, license, specialties
- Links with click tracking and icons
- Testimonials with star ratings
- Property listings with photo, price, beds/baths
- Widgets: mortgage calculator, home valuation, Calendly booking
- Contact / lead capture form
- Theme system: 8 base themes, 14 accent colors, font and radius controls

**Dashboard**

- Analytics: 7-day page views, link clicks, lead submissions
- Lead inbox + CRM integrations (Zapier, Follow Up Boss, Mailchimp)
- Billing portal: plan switching, invoice history
- Multi-session management with browser/OS tracking
- Live preview of public page

## Subscriptions

| Plan | Price |
| -------- | ---------------------- |
| **Solo** | $19/mo · $15/mo annual |
| **Pro** | $39/mo · $31/mo annual |

Pro unlocks: listings, widgets (mortgage calculator, home valuation, Calendly), and CRM integrations (Zapier, Follow Up Boss, Mailchimp). New accounts get a 14-day trial.

Stripe handles checkout, billing portal, and webhooks. On new checkout, prior active subscriptions are automatically cancelled. Agents receive email notifications on payment failures and cancellation.

## SEO

- Per-agent dynamic OG metadata (name, bio, avatar)
- Schema.org `RealEstateAgent` JSON-LD with `AggregateRating`
- ISR on public pages (revalidate every 3600s)
- Environment-aware `robots.txt` (blocks crawlers on dev/preview)
- Vercel Analytics + Speed Insights

## Technical Highlights

- **Auth** — Lucia sessions (12-week expiry) with IP/browser/OS tracking. bcrypt for passwords. Google OAuth via Arctic.
- **Rate limiting** — DynamoDB sliding-window rate limiter with per-endpoint limits (e.g. login: 5 req/3min, contact: 3 req/hr). Graceful degradation on DynamoDB outage.
- **Image pipeline** — Presigned S3 uploads (5MB max, JPEG/PNG/WebP), served via CloudFront CDN. Old files deleted on replacement.
- **CRM integrations** — Lead submissions fan out asynchronously post-response to Zapier, Follow Up Boss, and Mailchimp. Non-blocking — does not affect response time.
- **Infrastructure as code** — AWS resources (S3, CloudFront, DynamoDB) provisioned with Pulumi. Separate dev/prod stacks.
- **Theme system** — Fully composable: base theme + accent color + button radius + font family/size. Applied via Tailwind classes and inline hex values; dashboard (DaisyUI) and public page use separate styling layers.

## Possible Improvements

- Queue/retry mechanism for CRM webhook delivery (currently fire-and-forget)
- Activate remaining i18n locales (FR, DE, ES, PT, IT — config exists, routing disabled)
- Analytics event batching/sampling at scale
- Email format validation before forwarding leads to Follow Up Boss
- Complete Google Reviews auto-pull (referenced in upgrade modal, not yet implemented)

## License

Expand Down
Loading