├── .eslintrc.json
├── app
├── favicon.ico
├── fonts
│ ├── GeistVF.woff
│ └── GeistMonoVF.woff
├── layout.tsx
├── globals.css
└── page.tsx
├── README.md
├── next.config.mjs
├── postcss.config.mjs
├── lib
└── utils.ts
├── server
└── db
│ ├── index.ts
│ └── schema.ts
├── drizzle.config.ts
├── components.json
├── .gitignore
├── middleware.ts
├── tsconfig.json
├── package.json
├── tailwind.config.ts
└── components
└── ui
└── button.tsx
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["next/core-web-vitals", "next/typescript"]
3 | }
4 |
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bhancockio/ai-real-estate-offers/HEAD/app/favicon.ico
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Link to Claude prompts:
2 |
3 | https://github.com/bhancockio/claude-crash-course-templates
4 |
--------------------------------------------------------------------------------
/app/fonts/GeistVF.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bhancockio/ai-real-estate-offers/HEAD/app/fonts/GeistVF.woff
--------------------------------------------------------------------------------
/app/fonts/GeistMonoVF.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bhancockio/ai-real-estate-offers/HEAD/app/fonts/GeistMonoVF.woff
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {};
3 |
4 | export default nextConfig;
5 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/server/db/index.ts:
--------------------------------------------------------------------------------
1 | import { sql } from "@vercel/postgres";
2 | import { drizzle } from "drizzle-orm/vercel-postgres";
3 | import { config } from "dotenv";
4 |
5 | config({ path: ".env.local" }); // or .env
6 |
7 | import * as schema from "./schema";
8 |
9 | export const db = drizzle(sql, { schema });
10 |
--------------------------------------------------------------------------------
/drizzle.config.ts:
--------------------------------------------------------------------------------
1 | import { config } from "dotenv";
2 | import { defineConfig } from "drizzle-kit";
3 |
4 | config({ path: ".env.local" });
5 |
6 | export default defineConfig({
7 | schema: "./server/db/schema.ts",
8 | out: "./migrations",
9 | dialect: "postgresql",
10 | dbCredentials: {
11 | url: process.env.POSTGRES_URL!,
12 | },
13 | });
14 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "app/globals.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | }
20 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 | .env
31 |
32 | # vercel
33 | .vercel
34 |
35 | # typescript
36 | *.tsbuildinfo
37 | next-env.d.ts
38 |
--------------------------------------------------------------------------------
/middleware.ts:
--------------------------------------------------------------------------------
1 | import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
2 |
3 | const isPublicRoute = createRouteMatcher(["/"]);
4 |
5 | export default clerkMiddleware((auth, request) => {
6 | if (!auth().userId && !isPublicRoute(request)) {
7 | auth().protect();
8 | }
9 | });
10 |
11 | export const config = {
12 | matcher: [
13 | // Skip Next.js internals and all static files, unless found in search params
14 | "/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
15 | // Always run for API routes
16 | "/(api|trpc)(.*)",
17 | ],
18 | };
19 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------
/server/db/schema.ts:
--------------------------------------------------------------------------------
1 | import { uuid, pgTable, text, timestamp, varchar } from "drizzle-orm/pg-core"; // Assuming 'uuid' is the correct import for UUIDs
2 |
3 | export const offersTable = pgTable("offers_table", {
4 | id: uuid("id").primaryKey().defaultRandom(), // Use UUID with a default random value
5 | userId: varchar("user_id", { length: 50 }).notNull(), // Assuming userId is also a UUID
6 | clientName: text("client_name").notNull(), // Added field for client's name
7 | clientAddress: text("client_address").notNull(), // Added field for client's address
8 | content: text("content").default(""), // Make content optional with default ""
9 | createdAt: timestamp("created_at").notNull().defaultNow(),
10 | updatedAt: timestamp("updated_at")
11 | .notNull()
12 | .$onUpdate(() => new Date()),
13 | });
14 |
15 | export type InsertOffer = typeof offersTable.$inferInsert;
16 | export type Offer = typeof offersTable.$inferSelect;
17 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ai-real-estate-offers",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@clerk/nextjs": "^5.7.2",
13 | "@radix-ui/react-icons": "^1.3.0",
14 | "@radix-ui/react-slot": "^1.1.0",
15 | "@vercel/postgres": "^0.10.0",
16 | "class-variance-authority": "^0.7.0",
17 | "clsx": "^2.1.1",
18 | "dotenv": "^16.4.5",
19 | "drizzle-orm": "^0.34.1",
20 | "lucide-react": "^0.451.0",
21 | "next": "14.2.15",
22 | "react": "^18",
23 | "react-dom": "^18",
24 | "tailwind-merge": "^2.5.3",
25 | "tailwindcss-animate": "^1.0.7"
26 | },
27 | "devDependencies": {
28 | "@types/node": "^20",
29 | "@types/react": "^18",
30 | "@types/react-dom": "^18",
31 | "drizzle-kit": "^0.25.0",
32 | "eslint": "^8",
33 | "eslint-config-next": "14.2.15",
34 | "postcss": "^8",
35 | "tailwindcss": "^3.4.1",
36 | "typescript": "^5"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import localFont from "next/font/local";
3 | import "./globals.css";
4 | import {
5 | ClerkProvider,
6 | SignInButton,
7 | SignedIn,
8 | SignedOut,
9 | UserButton,
10 | } from "@clerk/nextjs";
11 |
12 | const geistSans = localFont({
13 | src: "./fonts/GeistVF.woff",
14 | variable: "--font-geist-sans",
15 | weight: "100 900",
16 | });
17 | const geistMono = localFont({
18 | src: "./fonts/GeistMonoVF.woff",
19 | variable: "--font-geist-mono",
20 | weight: "100 900",
21 | });
22 |
23 | export const metadata: Metadata = {
24 | title: "Create Next App",
25 | description: "Generated by create next app",
26 | };
27 |
28 | export default function RootLayout({
29 | children,
30 | }: Readonly<{
31 | children: React.ReactNode;
32 | }>) {
33 | return (
34 |
35 |
36 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | {children}
46 |
47 |
48 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | const config: Config = {
4 | darkMode: ["class"],
5 | content: [
6 | "./pages/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./components/**/*.{js,ts,jsx,tsx,mdx}",
8 | "./app/**/*.{js,ts,jsx,tsx,mdx}",
9 | ],
10 | theme: {
11 | extend: {
12 | colors: {
13 | background: 'hsl(var(--background))',
14 | foreground: 'hsl(var(--foreground))',
15 | card: {
16 | DEFAULT: 'hsl(var(--card))',
17 | foreground: 'hsl(var(--card-foreground))'
18 | },
19 | popover: {
20 | DEFAULT: 'hsl(var(--popover))',
21 | foreground: 'hsl(var(--popover-foreground))'
22 | },
23 | primary: {
24 | DEFAULT: 'hsl(var(--primary))',
25 | foreground: 'hsl(var(--primary-foreground))'
26 | },
27 | secondary: {
28 | DEFAULT: 'hsl(var(--secondary))',
29 | foreground: 'hsl(var(--secondary-foreground))'
30 | },
31 | muted: {
32 | DEFAULT: 'hsl(var(--muted))',
33 | foreground: 'hsl(var(--muted-foreground))'
34 | },
35 | accent: {
36 | DEFAULT: 'hsl(var(--accent))',
37 | foreground: 'hsl(var(--accent-foreground))'
38 | },
39 | destructive: {
40 | DEFAULT: 'hsl(var(--destructive))',
41 | foreground: 'hsl(var(--destructive-foreground))'
42 | },
43 | border: 'hsl(var(--border))',
44 | input: 'hsl(var(--input))',
45 | ring: 'hsl(var(--ring))',
46 | chart: {
47 | '1': 'hsl(var(--chart-1))',
48 | '2': 'hsl(var(--chart-2))',
49 | '3': 'hsl(var(--chart-3))',
50 | '4': 'hsl(var(--chart-4))',
51 | '5': 'hsl(var(--chart-5))'
52 | }
53 | },
54 | borderRadius: {
55 | lg: 'var(--radius)',
56 | md: 'calc(var(--radius) - 2px)',
57 | sm: 'calc(var(--radius) - 4px)'
58 | }
59 | }
60 | },
61 | plugins: [require("tailwindcss-animate")],
62 | };
63 | export default config;
64 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "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",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
16 | outline:
17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
20 | ghost: "hover:bg-accent hover:text-accent-foreground",
21 | link: "text-primary underline-offset-4 hover:underline",
22 | },
23 | size: {
24 | default: "h-9 px-4 py-2",
25 | sm: "h-8 rounded-md px-3 text-xs",
26 | lg: "h-10 rounded-md px-8",
27 | icon: "h-9 w-9",
28 | },
29 | },
30 | defaultVariants: {
31 | variant: "default",
32 | size: "default",
33 | },
34 | }
35 | )
36 |
37 | export interface ButtonProps
38 | extends React.ButtonHTMLAttributes,
39 | VariantProps {
40 | asChild?: boolean
41 | }
42 |
43 | const Button = React.forwardRef(
44 | ({ className, variant, size, asChild = false, ...props }, ref) => {
45 | const Comp = asChild ? Slot : "button"
46 | return (
47 |
52 | )
53 | }
54 | )
55 | Button.displayName = "Button"
56 |
57 | export { Button, buttonVariants }
58 |
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | body {
6 | font-family: Arial, Helvetica, sans-serif;
7 | }
8 |
9 | @layer utilities {
10 | .text-balance {
11 | text-wrap: balance;
12 | }
13 | }
14 |
15 | @layer base {
16 | :root {
17 | --background: 0 0% 100%;
18 | --foreground: 0 0% 3.9%;
19 | --card: 0 0% 100%;
20 | --card-foreground: 0 0% 3.9%;
21 | --popover: 0 0% 100%;
22 | --popover-foreground: 0 0% 3.9%;
23 | --primary: 0 0% 9%;
24 | --primary-foreground: 0 0% 98%;
25 | --secondary: 0 0% 96.1%;
26 | --secondary-foreground: 0 0% 9%;
27 | --muted: 0 0% 96.1%;
28 | --muted-foreground: 0 0% 45.1%;
29 | --accent: 0 0% 96.1%;
30 | --accent-foreground: 0 0% 9%;
31 | --destructive: 0 84.2% 60.2%;
32 | --destructive-foreground: 0 0% 98%;
33 | --border: 0 0% 89.8%;
34 | --input: 0 0% 89.8%;
35 | --ring: 0 0% 3.9%;
36 | --chart-1: 12 76% 61%;
37 | --chart-2: 173 58% 39%;
38 | --chart-3: 197 37% 24%;
39 | --chart-4: 43 74% 66%;
40 | --chart-5: 27 87% 67%;
41 | --radius: 0.5rem;
42 | }
43 | .dark {
44 | --background: 0 0% 3.9%;
45 | --foreground: 0 0% 98%;
46 | --card: 0 0% 3.9%;
47 | --card-foreground: 0 0% 98%;
48 | --popover: 0 0% 3.9%;
49 | --popover-foreground: 0 0% 98%;
50 | --primary: 0 0% 98%;
51 | --primary-foreground: 0 0% 9%;
52 | --secondary: 0 0% 14.9%;
53 | --secondary-foreground: 0 0% 98%;
54 | --muted: 0 0% 14.9%;
55 | --muted-foreground: 0 0% 63.9%;
56 | --accent: 0 0% 14.9%;
57 | --accent-foreground: 0 0% 98%;
58 | --destructive: 0 62.8% 30.6%;
59 | --destructive-foreground: 0 0% 98%;
60 | --border: 0 0% 14.9%;
61 | --input: 0 0% 14.9%;
62 | --ring: 0 0% 83.1%;
63 | --chart-1: 220 70% 50%;
64 | --chart-2: 160 60% 45%;
65 | --chart-3: 30 80% 55%;
66 | --chart-4: 280 65% 60%;
67 | --chart-5: 340 75% 55%;
68 | }
69 | }
70 |
71 | @layer base {
72 | * {
73 | @apply border-border;
74 | }
75 | body {
76 | @apply bg-background text-foreground;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 |
3 | export default function Home() {
4 | return (
5 |
6 |
7 |
15 |
16 | -
17 | Get started by editing{" "}
18 |
19 | app/page.tsx
20 |
21 | .
22 |
23 | - Save and see your changes instantly.
24 |
25 |
26 |
51 |
52 |
99 |
100 | );
101 | }
102 |
--------------------------------------------------------------------------------