70 | >(({ className, ...props }, ref) => (
71 |
76 | ))
77 | CardFooter.displayName = "CardFooter"
78 |
79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
80 |
--------------------------------------------------------------------------------
/app/favorites/page.tsx:
--------------------------------------------------------------------------------
1 | import { getKindeServerSession } from "@kinde-oss/kinde-auth-nextjs/server";
2 | import prisma from "../lib/db";
3 | import { redirect } from "next/navigation";
4 | import { NoItems } from "../components/NoItem";
5 | import { ListingCard } from "../components/ListingCard";
6 | import { unstable_noStore as noStore } from "next/cache";
7 |
8 | async function getData(userId: string) {
9 | noStore();
10 | const data = await prisma.favorite.findMany({
11 | where: {
12 | userId: userId,
13 | },
14 | select: {
15 | Home: {
16 | select: {
17 | photo: true,
18 | id: true,
19 | Favorite: true,
20 | price: true,
21 | country: true,
22 | description: true,
23 | },
24 | },
25 | },
26 | });
27 |
28 | return data;
29 | }
30 |
31 | export default async function FavoriteRoute() {
32 | const { getUser } = getKindeServerSession();
33 | const user = await getUser();
34 | if (!user) return redirect("/");
35 | const data = await getData(user.id);
36 |
37 | return (
38 |
39 | Your Favorites
40 |
41 | {data.length === 0 ? (
42 |
46 | ) : (
47 |
48 | {data.map((item) => (
49 | 0 ? true : false
61 | }
62 | />
63 | ))}
64 |
65 | )}
66 |
67 | );
68 | }
69 |
--------------------------------------------------------------------------------
/app/create/[id]/address/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { createLocation } from "@/app/actions";
4 | import { CreatioBottomBar } from "@/app/components/CreationBottomBar";
5 | import { useCountries } from "@/app/lib/getCountries";
6 | import {
7 | Select,
8 | SelectContent,
9 | SelectGroup,
10 | SelectItem,
11 | SelectLabel,
12 | SelectTrigger,
13 | SelectValue,
14 | } from "@/components/ui/select";
15 | import { Skeleton } from "@/components/ui/skeleton";
16 | import dynamic from "next/dynamic";
17 | import { useState } from "react";
18 |
19 | export default function AddressRoutw({ params }: { params: { id: string } }) {
20 | const { getAllCountries } = useCountries();
21 | const [locationValue, setLocationValue] = useState("");
22 |
23 | const LazyMap = dynamic(() => import("@/app/components/Map"), {
24 | ssr: false,
25 | loading: () => ,
26 | });
27 | return (
28 | <>
29 |
30 |
31 | Where is your Home located?
32 |
33 |
34 |
35 |
62 | >
63 | );
64 | }
65 |
--------------------------------------------------------------------------------
/app/my-homes/page.tsx:
--------------------------------------------------------------------------------
1 | import { getKindeServerSession } from "@kinde-oss/kinde-auth-nextjs/server";
2 | import prisma from "../lib/db";
3 | import { redirect } from "next/navigation";
4 | import { NoItems } from "../components/NoItem";
5 | import { ListingCard } from "../components/ListingCard";
6 | import { unstable_noStore as noStore } from "next/cache";
7 |
8 | async function getData(userId: string) {
9 | noStore();
10 | const data = await prisma.home.findMany({
11 | where: {
12 | userId: userId,
13 | addedCategory: true,
14 | addedDescription: true,
15 | addedLoaction: true,
16 | },
17 | select: {
18 | id: true,
19 | country: true,
20 | photo: true,
21 | description: true,
22 | price: true,
23 | Favorite: {
24 | where: {
25 | userId: userId,
26 | },
27 | },
28 | },
29 | orderBy: {
30 | createdAT: "desc",
31 | },
32 | });
33 |
34 | return data;
35 | }
36 |
37 | export default async function MyHomes() {
38 | const { getUser } = getKindeServerSession();
39 | const user = await getUser();
40 |
41 | if (!user) {
42 | return redirect("/");
43 | }
44 | const data = await getData(user.id);
45 | return (
46 |
47 | Your Homes
48 |
49 | {data.length === 0 ? (
50 |
54 | ) : (
55 |
56 | {data.map((item) => (
57 | 0 ? true : false}
68 | />
69 | ))}
70 |
71 | )}
72 |
73 | );
74 | }
75 |
--------------------------------------------------------------------------------
/app/reservations/page.tsx:
--------------------------------------------------------------------------------
1 | import { getKindeServerSession } from "@kinde-oss/kinde-auth-nextjs/server";
2 | import { ListingCard } from "../components/ListingCard";
3 | import { NoItems } from "../components/NoItem";
4 | import prisma from "../lib/db";
5 | import { redirect } from "next/navigation";
6 | import { unstable_noStore as noStore } from "next/cache";
7 |
8 | async function getData(userId: string) {
9 | noStore();
10 | const data = await prisma.reservation.findMany({
11 | where: {
12 | userId: userId,
13 | },
14 | select: {
15 | Home: {
16 | select: {
17 | id: true,
18 | country: true,
19 | photo: true,
20 | description: true,
21 | price: true,
22 | Favorite: {
23 | where: {
24 | userId: userId,
25 | },
26 | },
27 | },
28 | },
29 | },
30 | });
31 |
32 | return data;
33 | }
34 |
35 | export default async function ReservationsRoute() {
36 | const { getUser } = getKindeServerSession();
37 | const user = await getUser();
38 | if (!user?.id) return redirect("/");
39 | const data = await getData(user.id);
40 | return (
41 |
42 |
43 | Your Reservations
44 |
45 |
46 | {data.length === 0 ? (
47 |
51 | ) : (
52 |
53 | {data.map((item) => (
54 | 0 ? true : false
66 | }
67 | />
68 | ))}
69 |
70 | )}
71 |
72 | );
73 | }
74 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss"
2 |
3 | const config = {
4 | darkMode: ["class"],
5 | content: [
6 | './pages/**/*.{ts,tsx}',
7 | './components/**/*.{ts,tsx}',
8 | './app/**/*.{ts,tsx}',
9 | './src/**/*.{ts,tsx}',
10 | ],
11 | prefix: "",
12 | theme: {
13 | container: {
14 | center: true,
15 | padding: "2rem",
16 | screens: {
17 | "2xl": "1400px",
18 | },
19 | },
20 | extend: {
21 | colors: {
22 | border: "hsl(var(--border))",
23 | input: "hsl(var(--input))",
24 | ring: "hsl(var(--ring))",
25 | background: "hsl(var(--background))",
26 | foreground: "hsl(var(--foreground))",
27 | primary: {
28 | DEFAULT: "hsl(var(--primary))",
29 | foreground: "hsl(var(--primary-foreground))",
30 | },
31 | secondary: {
32 | DEFAULT: "hsl(var(--secondary))",
33 | foreground: "hsl(var(--secondary-foreground))",
34 | },
35 | destructive: {
36 | DEFAULT: "hsl(var(--destructive))",
37 | foreground: "hsl(var(--destructive-foreground))",
38 | },
39 | muted: {
40 | DEFAULT: "hsl(var(--muted))",
41 | foreground: "hsl(var(--muted-foreground))",
42 | },
43 | accent: {
44 | DEFAULT: "hsl(var(--accent))",
45 | foreground: "hsl(var(--accent-foreground))",
46 | },
47 | popover: {
48 | DEFAULT: "hsl(var(--popover))",
49 | foreground: "hsl(var(--popover-foreground))",
50 | },
51 | card: {
52 | DEFAULT: "hsl(var(--card))",
53 | foreground: "hsl(var(--card-foreground))",
54 | },
55 | },
56 | borderRadius: {
57 | lg: "var(--radius)",
58 | md: "calc(var(--radius) - 2px)",
59 | sm: "calc(var(--radius) - 4px)",
60 | },
61 | keyframes: {
62 | "accordion-down": {
63 | from: { height: "0" },
64 | to: { height: "var(--radix-accordion-content-height)" },
65 | },
66 | "accordion-up": {
67 | from: { height: "var(--radix-accordion-content-height)" },
68 | to: { height: "0" },
69 | },
70 | },
71 | animation: {
72 | "accordion-down": "accordion-down 0.2s ease-out",
73 | "accordion-up": "accordion-up 0.2s ease-out",
74 | },
75 | },
76 | },
77 | plugins: [require("tailwindcss-animate")],
78 | } satisfies Config
79 |
80 | export default config
--------------------------------------------------------------------------------
/app/components/SubmitButtons.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import { Heart, Loader2 } from "lucide-react";
5 | import { useFormStatus } from "react-dom";
6 |
7 | export function CreationSubmit() {
8 | const { pending } = useFormStatus();
9 | return (
10 | <>
11 | {pending ? (
12 |
16 | ) : (
17 |
20 | )}
21 | >
22 | );
23 | }
24 |
25 | export function AddToFavoriteButton() {
26 | const { pending } = useFormStatus();
27 | return (
28 | <>
29 | {pending ? (
30 |
38 | ) : (
39 |
47 | )}
48 | >
49 | );
50 | }
51 |
52 | export function DeleteFromFavoriteButton() {
53 | const { pending } = useFormStatus();
54 | return (
55 | <>
56 | {pending ? (
57 |
65 | ) : (
66 |
74 | )}
75 | >
76 | );
77 | }
78 |
79 | export function ReservationSubmitButton() {
80 | const { pending } = useFormStatus();
81 |
82 | return (
83 | <>
84 | {pending ? (
85 |
88 | ) : (
89 |
92 | )}
93 | >
94 | );
95 | }
96 |
--------------------------------------------------------------------------------
/app/components/ListingCard.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import Link from "next/link";
3 | import { useCountries } from "../lib/getCountries";
4 | import { AddToFavoriteButton, DeleteFromFavoriteButton } from "./SubmitButtons";
5 | import { DeleteFromFavorite, addToFavorite } from "../actions";
6 |
7 | interface iAppProps {
8 | imagePath: string;
9 | description: string;
10 | location: string;
11 | price: number;
12 | userId: string | undefined;
13 | isInFavoriteList: boolean;
14 | favoriteId: string;
15 | homeId: string;
16 | pathName: string;
17 | }
18 |
19 | export function ListingCard({
20 | description,
21 | imagePath,
22 | location,
23 | price,
24 | userId,
25 | favoriteId,
26 | homeId,
27 | isInFavoriteList,
28 | pathName,
29 | }: iAppProps) {
30 | const { getCountryByValue } = useCountries();
31 | const country = getCountryByValue(location);
32 |
33 | return (
34 |
35 |
36 |
42 |
43 | {userId && (
44 |
45 | {isInFavoriteList ? (
46 |
52 | ) : (
53 |
59 | )}
60 |
61 | )}
62 |
63 |
64 |
65 |
66 | {country?.flag} {country?.label} / {country?.region}
67 |
68 |
69 | {description}
70 |
71 |
72 | ${price} Night
73 |
74 |
75 |
76 | );
77 | }
78 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 🚀 Build an Airbnb Clone with Next.js 14, Kinde, Supabase, Prisma, Tailwind and Shadcn/UI! Learn step-by-step and elevate your development skills.
2 |
3 | - 🚀 Kinde Auth: https://dub.sh/xeU8r3v
4 |
5 |
6 | - 👨🏻💻 GitHub Repository: https://www.janmarshal.com/courses/create-an-airbnb-clone-with-next-js-14-kinde-supabase-prisma-and-tailwind
7 | - 🌍 My Website: https://www.janmarshal.com
8 | - 📧 Business ONLY: jan@alenix.de
9 |
10 | Resources used:
11 | - Next.js: https://nextjs.org
12 | - Kinde: https://dub.sh/xeU8r3v
13 | - Tailwind.css: https://tailwindcss.com
14 | - Shadcn/UI: https://ui.shadcn.com
15 | - Prisma: https://prisma.io
16 | - Supabase: https://supabase.com
17 | - React-date-range: https://www.npmjs.com/package/react-date-range
18 |
19 | Features:
20 |
21 | - 🌐 Next.js 14 App Router
22 | - 🔐 Kinde Authentication
23 | - 📧 Passwordless Auth
24 | - 🔑 OAuth (Google and Facebook)
25 | - 💿 Supabase Database
26 | - 🖼️ Supabase Storage
27 | - 💨 Prisma ORM
28 | - 🎨 Styling with Tailwindcss and shadcn UI
29 | - Deployment to Vercel
30 | - 📅 Calendar Implementation
31 | - 📍 Dynamic Map Implementation
32 | - 📒 Reservation System
33 | - 🧠 Filter Bar
34 | - 🔎 Multi Step Search Modal
35 | - 📝 Multi Step Form Listing Creation
36 |
37 | - Streaming with Suspense Boundaries
38 | - Pending States
39 | - Caching
40 | - Authentication with customized login page
41 | - Server side implementation
42 | - Speed optimization
43 |
44 |
45 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
46 |
47 | ## Getting Started
48 |
49 | First, run the development server:
50 |
51 | ```bash
52 | npm run dev
53 | # or
54 | yarn dev
55 | # or
56 | pnpm dev
57 | # or
58 | bun dev
59 | ```
60 |
61 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
62 |
63 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
64 |
65 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
66 |
67 | ## Learn More
68 |
69 | To learn more about Next.js, take a look at the following resources:
70 |
71 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
72 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
73 |
74 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
75 |
76 | ## Deploy on Vercel
77 |
78 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
79 |
80 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
81 |
--------------------------------------------------------------------------------
/app/components/UserNav.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable @next/next/no-img-element */
2 | import {
3 | DropdownMenu,
4 | DropdownMenuContent,
5 | DropdownMenuItem,
6 | DropdownMenuSeparator,
7 | DropdownMenuTrigger,
8 | } from "@/components/ui/dropdown-menu";
9 | import { MenuIcon } from "lucide-react";
10 | import {
11 | RegisterLink,
12 | LoginLink,
13 | LogoutLink,
14 | } from "@kinde-oss/kinde-auth-nextjs/components";
15 | import { getKindeServerSession } from "@kinde-oss/kinde-auth-nextjs/server";
16 | import Link from "next/link";
17 | import { createAirbnbHome } from "../actions";
18 |
19 | export async function UserNav() {
20 | const { getUser } = getKindeServerSession();
21 | const user = await getUser();
22 |
23 | const createHomewithId = createAirbnbHome.bind(null, {
24 | userId: user?.id as string,
25 | });
26 |
27 | return (
28 |
29 |
30 |
31 |
32 |
33 |

41 |
42 |
43 |
44 | {user ? (
45 | <>
46 |
47 |
52 |
53 |
54 |
55 | My Listings
56 |
57 |
58 |
59 |
60 | My Favorites
61 |
62 |
63 |
64 |
65 | My Reservations
66 |
67 |
68 |
69 |
70 | Logout
71 |
72 | >
73 | ) : (
74 | <>
75 |
76 | Register
77 |
78 |
79 | Login
80 |
81 | >
82 | )}
83 |
84 |
85 | );
86 | }
87 |
--------------------------------------------------------------------------------
/app/lib/categoryItems.ts:
--------------------------------------------------------------------------------
1 | interface iAppProps {
2 | name: string;
3 | title: string;
4 | imageUrl: string;
5 | description: string;
6 | id: number;
7 | }
8 |
9 | export const categoryItems: iAppProps[] = [
10 | {
11 | id: 0,
12 | name: "beach",
13 | description: "This Property is close to the Beach.",
14 | title: "Beach",
15 | imageUrl:
16 | "https://a0.muscache.com/pictures/10ce1091-c854-40f3-a2fb-defc2995bcaf.jpg",
17 | },
18 | {
19 | id: 1,
20 | name: "trending",
21 | description: "This is a Property which is trending.",
22 | title: "Trending",
23 | imageUrl:
24 | "https://a0.muscache.com/pictures/3726d94b-534a-42b8-bca0-a0304d912260.jpg",
25 | },
26 | {
27 | id: 2,
28 | name: "beachfront",
29 | description: "This is a Property is close to the beachfront",
30 | title: "Beachfront",
31 | imageUrl:
32 | "https://a0.muscache.com/pictures/bcd1adc0-5cee-4d7a-85ec-f6730b0f8d0c.jpg",
33 | },
34 | {
35 | id: 3,
36 | name: "erathhome",
37 | description: "This Property is considerd a Earth Home",
38 | title: "Earth Home",
39 | imageUrl:
40 | "https://a0.muscache.com/pictures/d7445031-62c4-46d0-91c3-4f29f9790f7a.jpg",
41 | },
42 | {
43 | id: 4,
44 | name: "luxe",
45 | description: "This Property is considerd Luxorious",
46 | title: "Luxe",
47 | imageUrl:
48 | "https://a0.muscache.com/pictures/c8e2ed05-c666-47b6-99fc-4cb6edcde6b4.jpg",
49 | },
50 | {
51 | id: 5,
52 | name: "amazingView",
53 | description: "This property has an amazing View",
54 | title: "Amazing View",
55 | imageUrl:
56 | "https://a0.muscache.com/pictures/3b1eb541-46d9-4bef-abc4-c37d77e3c21b.jpg",
57 | },
58 | {
59 | id: 6,
60 | name: "design",
61 | description: "This property puts a big focus on design ",
62 | title: "Design",
63 | imageUrl:
64 | "https://a0.muscache.com/pictures/50861fca-582c-4bcc-89d3-857fb7ca6528.jpg",
65 | },
66 | {
67 | id: 7,
68 | name: "pool",
69 | description: "This property has an amazing Pool",
70 | title: "Pool",
71 | imageUrl:
72 | "https://a0.muscache.com/pictures/3fb523a0-b622-4368-8142-b5e03df7549b.jpg",
73 | },
74 | {
75 | id: 8,
76 | name: "tiny",
77 | description: "This property is considered a tiny home",
78 | title: "Tiny Home",
79 | imageUrl:
80 | "https://a0.muscache.com/pictures/3271df99-f071-4ecf-9128-eb2d2b1f50f0.jpg",
81 | },
82 | {
83 | id: 9,
84 | name: "historic",
85 | description: "This Property is considered historic",
86 | title: "Historic Home",
87 | imageUrl:
88 | "https://a0.muscache.com/pictures/33dd714a-7b4a-4654-aaf0-f58ea887a688.jpg",
89 | },
90 | {
91 | id: 10,
92 | name: "countryside",
93 | description: "This Property is located on the countryside",
94 | title: "Countryside",
95 | imageUrl:
96 | "https://a0.muscache.com/pictures/6ad4bd95-f086-437d-97e3-14d12155ddfe.jpg",
97 | },
98 | {
99 | id: 11,
100 | name: "omg",
101 | description: "This Property has a wow factor",
102 | title: "WOW!",
103 | imageUrl:
104 | "https://a0.muscache.com/pictures/c5a4f6fc-c92c-4ae8-87dd-57f1ff1b89a6.jpg",
105 | },
106 | {
107 | id: 12,
108 | name: "surfing",
109 | description: "This Property is located near to a surfing spot",
110 | title: "Surfing",
111 | imageUrl:
112 | "https://a0.muscache.com/pictures/957f8022-dfd7-426c-99fd-77ed792f6d7a.jpg",
113 | },
114 | ];
115 |
--------------------------------------------------------------------------------
/app/create/[id]/description/page.tsx:
--------------------------------------------------------------------------------
1 | import { CreateDescription } from "@/app/actions";
2 | import { Counter } from "@/app/components/Counter";
3 | import { CreatioBottomBar } from "@/app/components/CreationBottomBar";
4 | import { Card, CardHeader } from "@/components/ui/card";
5 | import { Input } from "@/components/ui/input";
6 | import { Label } from "@/components/ui/label";
7 | import { Textarea } from "@/components/ui/textarea";
8 |
9 | export default function DescriptionPage({
10 | params,
11 | }: {
12 | params: { id: string };
13 | }) {
14 | return (
15 | <>
16 |
17 |
18 | Please describe your home as good as you can!
19 |
20 |
21 |
22 |
97 | >
98 | );
99 | }
100 |
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { Suspense, use } from "react";
2 |
3 | import { MapFilterItems } from "./components/MapFilterItems";
4 | import prisma from "./lib/db";
5 | import { SkeltonCard } from "./components/SkeletonCard";
6 | import { NoItems } from "./components/NoItem";
7 | import { getKindeServerSession } from "@kinde-oss/kinde-auth-nextjs/server";
8 | import { ListingCard } from "./components/ListingCard";
9 | import { unstable_noStore as noStore } from "next/cache";
10 |
11 | async function getData({
12 | searchParams,
13 | userId,
14 | }: {
15 | userId: string | undefined;
16 | searchParams?: {
17 | filter?: string;
18 | country?: string;
19 | guest?: string;
20 | room?: string;
21 | bathroom?: string;
22 | };
23 | }) {
24 | noStore();
25 | const data = await prisma.home.findMany({
26 | where: {
27 | addedCategory: true,
28 | addedLoaction: true,
29 | addedDescription: true,
30 | categoryName: searchParams?.filter ?? undefined,
31 | country: searchParams?.country ?? undefined,
32 | guests: searchParams?.guest ?? undefined,
33 | bedrooms: searchParams?.room ?? undefined,
34 | bathrooms: searchParams?.bathroom ?? undefined,
35 | },
36 | select: {
37 | photo: true,
38 | id: true,
39 | price: true,
40 | description: true,
41 | country: true,
42 | Favorite: {
43 | where: {
44 | userId: userId ?? undefined,
45 | },
46 | },
47 | },
48 | });
49 |
50 | return data;
51 | }
52 |
53 | export default function Home({
54 | searchParams,
55 | }: {
56 | searchParams?: {
57 | filter?: string;
58 | country?: string;
59 | guest?: string;
60 | room?: string;
61 | bathroom?: string;
62 | };
63 | }) {
64 | return (
65 |
66 |
67 |
68 | }>
69 |
70 |
71 |
72 | );
73 | }
74 |
75 | async function ShowItems({
76 | searchParams,
77 | }: {
78 | searchParams?: {
79 | filter?: string;
80 | country?: string;
81 | guest?: string;
82 | room?: string;
83 | bathroom?: string;
84 | };
85 | }) {
86 | const { getUser } = getKindeServerSession();
87 | const user = await getUser();
88 | const data = await getData({ searchParams: searchParams, userId: user?.id });
89 |
90 | return (
91 | <>
92 | {data.length === 0 ? (
93 |
97 | ) : (
98 |
99 | {data.map((item) => (
100 | 0 ? true : false}
109 | homeId={item.id}
110 | pathName="/"
111 | />
112 | ))}
113 |
114 | )}
115 | >
116 | );
117 | }
118 |
119 | function SkeletonLoading() {
120 | return (
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 | );
133 | }
134 |
--------------------------------------------------------------------------------
/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DialogPrimitive from "@radix-ui/react-dialog"
5 | import { X } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = DialogPrimitive.Portal
14 |
15 | const DialogClose = DialogPrimitive.Close
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ))
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ))
54 | DialogContent.displayName = DialogPrimitive.Content.displayName
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | )
68 | DialogHeader.displayName = "DialogHeader"
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | )
82 | DialogFooter.displayName = "DialogFooter"
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ))
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogClose,
116 | DialogTrigger,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | }
123 |
--------------------------------------------------------------------------------
/app/home/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable @next/next/no-img-element */
2 |
3 | import { createReservation } from "@/app/actions";
4 | import { CaegoryShowcase } from "@/app/components/CategoryShowcase";
5 | import { HomeMap } from "@/app/components/HomeMap";
6 | import { SelectCalender } from "@/app/components/SelectCalender";
7 | import { ReservationSubmitButton } from "@/app/components/SubmitButtons";
8 | import prisma from "@/app/lib/db";
9 | import { useCountries } from "@/app/lib/getCountries";
10 | import { Button } from "@/components/ui/button";
11 | import { Separator } from "@/components/ui/separator";
12 | import { getKindeServerSession } from "@kinde-oss/kinde-auth-nextjs/server";
13 |
14 | import Image from "next/image";
15 | import Link from "next/link";
16 | import { unstable_noStore as noStore } from "next/cache";
17 |
18 | async function getData(homeid: string) {
19 | noStore();
20 | const data = await prisma.home.findUnique({
21 | where: {
22 | id: homeid,
23 | },
24 | select: {
25 | photo: true,
26 | description: true,
27 | guests: true,
28 | bedrooms: true,
29 | bathrooms: true,
30 | title: true,
31 | categoryName: true,
32 | price: true,
33 | country: true,
34 | Reservation: {
35 | where: {
36 | homeId: homeid,
37 | },
38 | },
39 |
40 | User: {
41 | select: {
42 | profileImage: true,
43 | firstName: true,
44 | },
45 | },
46 | },
47 | });
48 |
49 | return data;
50 | }
51 |
52 | export default async function HomeRoute({
53 | params,
54 | }: {
55 | params: { id: string };
56 | }) {
57 | const data = await getData(params.id);
58 | const { getCountryByValue } = useCountries();
59 | const country = getCountryByValue(data?.country as string);
60 | const { getUser } = getKindeServerSession();
61 | const user = await getUser();
62 | return (
63 |
64 |
{data?.title}
65 |
66 |
72 |
73 |
74 |
75 |
76 |
77 | {country?.flag} {country?.label} / {country?.region}
78 |
79 |
80 |
{data?.guests} Guests
*
{data?.bedrooms} Bedrooms
*{" "}
81 | {data?.bathrooms} Bathrooms
82 |
83 |
84 |
85 |

93 |
94 |
Hosted by {data?.User?.firstName}
95 |
Host since 2015
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
{data?.description}
106 |
107 |
108 |
109 |
110 |
111 |
112 |
126 |
127 |
128 | );
129 | }
130 |
--------------------------------------------------------------------------------
/app/actions.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { redirect } from "next/navigation";
4 | import prisma from "./lib/db";
5 | import { supabase } from "./lib/supabase";
6 | import { revalidatePath } from "next/cache";
7 | import path from "path";
8 |
9 | export async function createAirbnbHome({ userId }: { userId: string }) {
10 | const data = await prisma.home.findFirst({
11 | where: {
12 | userId: userId,
13 | },
14 | orderBy: {
15 | createdAT: "desc",
16 | },
17 | });
18 |
19 | if (data === null) {
20 | const data = await prisma.home.create({
21 | data: {
22 | userId: userId,
23 | },
24 | });
25 |
26 | return redirect(`/create/${data.id}/structure`);
27 | } else if (
28 | !data.addedCategory &&
29 | !data.addedDescription &&
30 | !data.addedLoaction
31 | ) {
32 | return redirect(`/create/${data.id}/structure`);
33 | } else if (data.addedCategory && !data.addedDescription) {
34 | return redirect(`/create/${data.id}/description`);
35 | } else if (
36 | data.addedCategory &&
37 | data.addedDescription &&
38 | !data.addedLoaction
39 | ) {
40 | return redirect(`/create/${data.id}/address`);
41 | } else if (
42 | data.addedCategory &&
43 | data.addedDescription &&
44 | data.addedLoaction
45 | ) {
46 | const data = await prisma.home.create({
47 | data: {
48 | userId: userId,
49 | },
50 | });
51 |
52 | return redirect(`/create/${data.id}/structure`);
53 | }
54 | }
55 |
56 | export async function createCategoryPage(formData: FormData) {
57 | const categoryName = formData.get("categoryName") as string;
58 | const homeId = formData.get("homeId") as string;
59 | const data = await prisma.home.update({
60 | where: {
61 | id: homeId,
62 | },
63 | data: {
64 | categoryName: categoryName,
65 | addedCategory: true,
66 | },
67 | });
68 |
69 | return redirect(`/create/${homeId}/description`);
70 | }
71 |
72 | export async function CreateDescription(formData: FormData) {
73 | const title = formData.get("title") as string;
74 | const description = formData.get("description") as string;
75 | const price = formData.get("price");
76 | const imageFile = formData.get("image") as File;
77 | const homeId = formData.get("homeId") as string;
78 |
79 | const guestNumber = formData.get("guest") as string;
80 | const roomNumber = formData.get("room") as string;
81 | const bathroomsNumber = formData.get("bathroom") as string;
82 |
83 | const { data: imageData } = await supabase.storage
84 | .from("images")
85 | .upload(`${imageFile.name}-${new Date()}`, imageFile, {
86 | cacheControl: "2592000",
87 | contentType: "image/png",
88 | });
89 |
90 | const data = await prisma.home.update({
91 | where: {
92 | id: homeId,
93 | },
94 | data: {
95 | title: title,
96 | description: description,
97 | price: Number(price),
98 | bedrooms: roomNumber,
99 | bathrooms: bathroomsNumber,
100 | guests: guestNumber,
101 | photo: imageData?.path,
102 | addedDescription: true,
103 | },
104 | });
105 |
106 | return redirect(`/create/${homeId}/address`);
107 | }
108 |
109 | export async function createLocation(formData: FormData) {
110 | const homeId = formData.get("homeId") as string;
111 | const countryValue = formData.get("countryValue") as string;
112 | const data = await prisma.home.update({
113 | where: {
114 | id: homeId,
115 | },
116 | data: {
117 | addedLoaction: true,
118 | country: countryValue,
119 | },
120 | });
121 |
122 | return redirect("/");
123 | }
124 |
125 | export async function addToFavorite(formData: FormData) {
126 | const homeId = formData.get("homeId") as string;
127 | const userId = formData.get("userId") as string;
128 | const pathName = formData.get("pathName") as string;
129 |
130 | const data = await prisma.favorite.create({
131 | data: {
132 | homeId: homeId,
133 | userId: userId,
134 | },
135 | });
136 |
137 | revalidatePath(pathName);
138 | }
139 |
140 | export async function DeleteFromFavorite(formData: FormData) {
141 | const favoriteId = formData.get("favoriteId") as string;
142 | const pathName = formData.get("pathName") as string;
143 | const userId = formData.get("userId") as string;
144 |
145 | const data = await prisma.favorite.delete({
146 | where: {
147 | id: favoriteId,
148 | userId: userId,
149 | },
150 | });
151 |
152 | revalidatePath(pathName);
153 | }
154 |
155 | export async function createReservation(formData: FormData) {
156 | const userId = formData.get("userId") as string;
157 | const homeId = formData.get("homeId") as string;
158 | const startDate = formData.get("startDate") as string;
159 | const endDate = formData.get("endDate") as string;
160 |
161 | const data = await prisma.reservation.create({
162 | data: {
163 | userId: userId,
164 | endDate: endDate,
165 | startDate: startDate,
166 | homeId: homeId,
167 | },
168 | });
169 |
170 | return redirect("/");
171 | }
172 |
--------------------------------------------------------------------------------
/app/components/SearchComponent.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | Dialog,
5 | DialogContent,
6 | DialogDescription,
7 | DialogFooter,
8 | DialogHeader,
9 | DialogTitle,
10 | DialogTrigger,
11 | } from "@/components/ui/dialog";
12 | import {
13 | Select,
14 | SelectContent,
15 | SelectGroup,
16 | SelectItem,
17 | SelectLabel,
18 | SelectTrigger,
19 | SelectValue,
20 | } from "@/components/ui/select";
21 |
22 | import { Search } from "lucide-react";
23 | import { useState } from "react";
24 | import { useCountries } from "../lib/getCountries";
25 | import { HomeMap } from "./HomeMap";
26 | import { Button } from "@/components/ui/button";
27 | import { CreationSubmit } from "./SubmitButtons";
28 | import { Card, CardHeader } from "@/components/ui/card";
29 | import { Counter } from "./Counter";
30 |
31 | export function SearchModalCompnent() {
32 | const [step, setStep] = useState(1);
33 | const [locationValue, setLocationValue] = useState("");
34 | const { getAllCountries } = useCountries();
35 |
36 | function SubmitButtonLocal() {
37 | if (step === 1) {
38 | return (
39 |
42 | );
43 | } else if (step === 2) {
44 | return ;
45 | }
46 | }
47 | return (
48 |
145 | );
146 | }
147 |
--------------------------------------------------------------------------------
/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SelectPrimitive from "@radix-ui/react-select"
5 | import { Check, ChevronDown, ChevronUp } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Select = SelectPrimitive.Root
10 |
11 | const SelectGroup = SelectPrimitive.Group
12 |
13 | const SelectValue = SelectPrimitive.Value
14 |
15 | const SelectTrigger = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, children, ...props }, ref) => (
19 | span]:line-clamp-1",
23 | className
24 | )}
25 | {...props}
26 | >
27 | {children}
28 |
29 |
30 |
31 |
32 | ))
33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
34 |
35 | const SelectScrollUpButton = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 |
48 |
49 | ))
50 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
51 |
52 | const SelectScrollDownButton = React.forwardRef<
53 | React.ElementRef,
54 | React.ComponentPropsWithoutRef
55 | >(({ className, ...props }, ref) => (
56 |
64 |
65 |
66 | ))
67 | SelectScrollDownButton.displayName =
68 | SelectPrimitive.ScrollDownButton.displayName
69 |
70 | const SelectContent = React.forwardRef<
71 | React.ElementRef,
72 | React.ComponentPropsWithoutRef
73 | >(({ className, children, position = "popper", ...props }, ref) => (
74 |
75 |
86 |
87 |
94 | {children}
95 |
96 |
97 |
98 |
99 | ))
100 | SelectContent.displayName = SelectPrimitive.Content.displayName
101 |
102 | const SelectLabel = React.forwardRef<
103 | React.ElementRef,
104 | React.ComponentPropsWithoutRef
105 | >(({ className, ...props }, ref) => (
106 |
111 | ))
112 | SelectLabel.displayName = SelectPrimitive.Label.displayName
113 |
114 | const SelectItem = React.forwardRef<
115 | React.ElementRef,
116 | React.ComponentPropsWithoutRef
117 | >(({ className, children, ...props }, ref) => (
118 |
126 |
127 |
128 |
129 |
130 |
131 |
132 | {children}
133 |
134 | ))
135 | SelectItem.displayName = SelectPrimitive.Item.displayName
136 |
137 | const SelectSeparator = React.forwardRef<
138 | React.ElementRef,
139 | React.ComponentPropsWithoutRef
140 | >(({ className, ...props }, ref) => (
141 |
146 | ))
147 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
148 |
149 | export {
150 | Select,
151 | SelectGroup,
152 | SelectValue,
153 | SelectTrigger,
154 | SelectContent,
155 | SelectLabel,
156 | SelectItem,
157 | SelectSeparator,
158 | SelectScrollUpButton,
159 | SelectScrollDownButton,
160 | }
161 |
--------------------------------------------------------------------------------
/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5 | import { Check, ChevronRight, Circle } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const DropdownMenu = DropdownMenuPrimitive.Root
10 |
11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
12 |
13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group
14 |
15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal
16 |
17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub
18 |
19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
20 |
21 | const DropdownMenuSubTrigger = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef & {
24 | inset?: boolean
25 | }
26 | >(({ className, inset, children, ...props }, ref) => (
27 |
36 | {children}
37 |
38 |
39 | ))
40 | DropdownMenuSubTrigger.displayName =
41 | DropdownMenuPrimitive.SubTrigger.displayName
42 |
43 | const DropdownMenuSubContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, ...props }, ref) => (
47 |
55 | ))
56 | DropdownMenuSubContent.displayName =
57 | DropdownMenuPrimitive.SubContent.displayName
58 |
59 | const DropdownMenuContent = React.forwardRef<
60 | React.ElementRef,
61 | React.ComponentPropsWithoutRef
62 | >(({ className, sideOffset = 4, ...props }, ref) => (
63 |
64 |
73 |
74 | ))
75 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
76 |
77 | const DropdownMenuItem = React.forwardRef<
78 | React.ElementRef,
79 | React.ComponentPropsWithoutRef & {
80 | inset?: boolean
81 | }
82 | >(({ className, inset, ...props }, ref) => (
83 |
92 | ))
93 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
94 |
95 | const DropdownMenuCheckboxItem = React.forwardRef<
96 | React.ElementRef,
97 | React.ComponentPropsWithoutRef
98 | >(({ className, children, checked, ...props }, ref) => (
99 |
108 |
109 |
110 |
111 |
112 |
113 | {children}
114 |
115 | ))
116 | DropdownMenuCheckboxItem.displayName =
117 | DropdownMenuPrimitive.CheckboxItem.displayName
118 |
119 | const DropdownMenuRadioItem = React.forwardRef<
120 | React.ElementRef,
121 | React.ComponentPropsWithoutRef
122 | >(({ className, children, ...props }, ref) => (
123 |
131 |
132 |
133 |
134 |
135 |
136 | {children}
137 |
138 | ))
139 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
140 |
141 | const DropdownMenuLabel = React.forwardRef<
142 | React.ElementRef,
143 | React.ComponentPropsWithoutRef & {
144 | inset?: boolean
145 | }
146 | >(({ className, inset, ...props }, ref) => (
147 |
156 | ))
157 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
158 |
159 | const DropdownMenuSeparator = React.forwardRef<
160 | React.ElementRef,
161 | React.ComponentPropsWithoutRef
162 | >(({ className, ...props }, ref) => (
163 |
168 | ))
169 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
170 |
171 | const DropdownMenuShortcut = ({
172 | className,
173 | ...props
174 | }: React.HTMLAttributes) => {
175 | return (
176 |
180 | )
181 | }
182 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
183 |
184 | export {
185 | DropdownMenu,
186 | DropdownMenuTrigger,
187 | DropdownMenuContent,
188 | DropdownMenuItem,
189 | DropdownMenuCheckboxItem,
190 | DropdownMenuRadioItem,
191 | DropdownMenuLabel,
192 | DropdownMenuSeparator,
193 | DropdownMenuShortcut,
194 | DropdownMenuGroup,
195 | DropdownMenuPortal,
196 | DropdownMenuSub,
197 | DropdownMenuSubContent,
198 | DropdownMenuSubTrigger,
199 | DropdownMenuRadioGroup,
200 | }
201 |
--------------------------------------------------------------------------------