├── .gitignore ├── 01-nextjs-tutorial ├── .eslintrc.json ├── .gitignore ├── README.md ├── app │ ├── (dashboard) │ │ └── auth │ │ │ └── [[...sign-in]] │ │ │ └── page.tsx │ ├── about │ │ └── page.tsx │ ├── actions │ │ └── page.tsx │ ├── api │ │ └── users │ │ │ └── route.ts │ ├── counter │ │ └── page.tsx │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ ├── page.tsx │ └── tours │ │ ├── [id] │ │ └── page.tsx │ │ ├── error.tsx │ │ ├── layout.tsx │ │ ├── loading.tsx │ │ └── page.tsx ├── components │ ├── Counter.tsx │ ├── DeleteButton.tsx │ ├── Form.tsx │ ├── Navbar.tsx │ └── UsersList.tsx ├── images │ └── maps.jpg ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.js ├── public │ ├── next.svg │ └── vercel.svg ├── tailwind.config.ts ├── tsconfig.json ├── users.json └── utils │ └── actions.ts ├── 02-home-away-project ├── .eslintrc.json ├── .gitignore ├── README.md ├── app │ ├── admin │ │ └── page.tsx │ ├── api │ │ ├── confirm │ │ │ └── route.ts │ │ └── payment │ │ │ └── route.ts │ ├── bookings │ │ ├── loading.tsx │ │ └── page.tsx │ ├── checkout │ │ └── page.tsx │ ├── favicon.ico │ ├── favorites │ │ ├── loading.tsx │ │ └── page.tsx │ ├── globals.css │ ├── layout.tsx │ ├── page.tsx │ ├── profile │ │ ├── create │ │ │ └── page.tsx │ │ └── page.tsx │ ├── properties │ │ ├── [id] │ │ │ └── page.tsx │ │ └── loading.tsx │ ├── providers.tsx │ ├── rentals │ │ ├── [id] │ │ │ └── edit │ │ │ │ └── page.tsx │ │ ├── create │ │ │ └── page.tsx │ │ ├── loading.tsx │ │ └── page.tsx │ ├── reservations │ │ ├── loading.tsx │ │ └── page.tsx │ ├── reviews │ │ ├── loading.tsx │ │ └── page.tsx │ └── theme-provider.tsx ├── components.json ├── components │ ├── admin │ │ ├── Chart.tsx │ │ ├── ChartsContainer.tsx │ │ ├── Loading.tsx │ │ ├── StatsCard.tsx │ │ └── StatsContainer.tsx │ ├── booking │ │ ├── BookingCalendar.tsx │ │ ├── BookingContainer.tsx │ │ ├── BookingForm.tsx │ │ ├── BookingWrapper.tsx │ │ ├── ConfirmBooking.tsx │ │ └── LoadingTable.tsx │ ├── card │ │ ├── CountryFlagAndName.tsx │ │ ├── FavoriteToggleButton.tsx │ │ ├── FavoriteToggleForm.tsx │ │ ├── LoadingCards.tsx │ │ ├── PropertyCard.tsx │ │ └── PropertyRating.tsx │ ├── form │ │ ├── AmenitiesInput.tsx │ │ ├── Buttons.tsx │ │ ├── CategoriesInput.tsx │ │ ├── CounterInput.tsx │ │ ├── CountriesInput.tsx │ │ ├── FormContainer.tsx │ │ ├── FormInput.tsx │ │ ├── ImageInput.tsx │ │ ├── ImageInputContainer.tsx │ │ ├── PriceInput.tsx │ │ ├── RatingInput.tsx │ │ └── TextAreaInput.tsx │ ├── home │ │ ├── CategoriesList.tsx │ │ ├── EmptyList.tsx │ │ ├── PropertiesContainer.tsx │ │ └── PropertiesList.tsx │ ├── navbar │ │ ├── DarkMode.tsx │ │ ├── LinksDropdown.tsx │ │ ├── Logo.tsx │ │ ├── NavSearch.tsx │ │ ├── Navbar.tsx │ │ ├── SignOutLink.tsx │ │ └── UserIcon.tsx │ ├── properties │ │ ├── Amenities.tsx │ │ ├── BreadCrumbs.tsx │ │ ├── Description.tsx │ │ ├── ImageContainer.tsx │ │ ├── PropertyDetails.tsx │ │ ├── PropertyMap.tsx │ │ ├── ShareButton.tsx │ │ ├── Title.tsx │ │ └── UserInfo.tsx │ ├── reservations │ │ └── Stats.tsx │ ├── reviews │ │ ├── Comment.tsx │ │ ├── PropertyReviews.tsx │ │ ├── Rating.tsx │ │ ├── ReviewCard.tsx │ │ └── SubmitReview.tsx │ └── ui │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── card.tsx │ │ ├── checkbox.tsx │ │ ├── dropdown-menu.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── popover.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── skeleton.tsx │ │ ├── table.tsx │ │ ├── textarea.tsx │ │ ├── toast.tsx │ │ ├── toaster.tsx │ │ └── use-toast.ts ├── lib │ └── utils.ts ├── middleware.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── prisma │ └── schema.prisma ├── public │ ├── images │ │ ├── 0-big-image.jpg │ │ ├── 0-user-peter.jpg │ │ ├── 0-user-susan.jpg │ │ ├── cabin-1.jpg │ │ ├── cabin-2.jpg │ │ ├── cabin-3.jpg │ │ ├── cabin-4.jpg │ │ ├── cabin-5.jpg │ │ ├── caravan-1.jpg │ │ ├── caravan-2.jpg │ │ ├── caravan-3.jpg │ │ ├── caravan-4.jpg │ │ ├── caravan-5.jpg │ │ ├── tent-1.jpg │ │ ├── tent-2.jpg │ │ ├── tent-3.jpg │ │ ├── tent-4.jpg │ │ └── tent-5.jpg │ ├── next.svg │ └── vercel.svg ├── tailwind.config.ts ├── tsconfig.json └── utils │ ├── actions.ts │ ├── amenities.ts │ ├── calculateTotals.ts │ ├── calendar.ts │ ├── categories.ts │ ├── countries.ts │ ├── db.ts │ ├── format.ts │ ├── links.ts │ ├── schemas.ts │ ├── store.ts │ ├── supabase.ts │ └── types.ts ├── 03-starter ├── .eslintrc.json ├── .gitignore ├── README.md ├── app │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ └── page.tsx ├── components.json ├── components │ └── ui │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── card.tsx │ │ ├── checkbox.tsx │ │ ├── dropdown-menu.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── popover.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── skeleton.tsx │ │ ├── table.tsx │ │ ├── textarea.tsx │ │ ├── toast.tsx │ │ ├── toaster.tsx │ │ └── use-toast.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public │ ├── images │ │ ├── 0-big-image.jpg │ │ ├── 0-user-peter.jpg │ │ ├── 0-user-susan.jpg │ │ ├── cabin-1.jpg │ │ ├── cabin-2.jpg │ │ ├── cabin-3.jpg │ │ ├── cabin-4.jpg │ │ ├── cabin-5.jpg │ │ ├── caravan-1.jpg │ │ ├── caravan-2.jpg │ │ ├── caravan-3.jpg │ │ ├── caravan-4.jpg │ │ ├── caravan-5.jpg │ │ ├── tent-1.jpg │ │ ├── tent-2.jpg │ │ ├── tent-3.jpg │ │ ├── tent-4.jpg │ │ └── tent-5.jpg │ ├── next.svg │ └── vercel.svg ├── tailwind.config.ts └── tsconfig.json └── README.md /.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 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /01-nextjs-tutorial/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /01-nextjs-tutorial/.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 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /01-nextjs-tutorial/app/(dashboard)/auth/[[...sign-in]]/page.tsx: -------------------------------------------------------------------------------- 1 | const SignInPage = ({ params }: { params: { 'sign-in': string[] } }) => { 2 | console.log(params); 3 | console.log(params['sign-in'][1]); 4 | return
SignInPage :{params['sign-in'][1]}
; 5 | }; 6 | export default SignInPage; 7 | -------------------------------------------------------------------------------- /01-nextjs-tutorial/app/about/page.tsx: -------------------------------------------------------------------------------- 1 | function AboutPage() { 2 | return ( 3 |
4 |

AboutPage

5 |
6 | ); 7 | } 8 | export default AboutPage; 9 | -------------------------------------------------------------------------------- /01-nextjs-tutorial/app/actions/page.tsx: -------------------------------------------------------------------------------- 1 | import Form from '@/components/Form'; 2 | import UsersList from '@/components/UsersList'; 3 | 4 | function ActionsPage() { 5 | return ( 6 | <> 7 |
8 | 9 | 10 | ); 11 | } 12 | export default ActionsPage; 13 | -------------------------------------------------------------------------------- /01-nextjs-tutorial/app/api/users/route.ts: -------------------------------------------------------------------------------- 1 | import { fetchUsers, saveUser } from '@/utils/actions'; 2 | import { NextRequest, NextResponse } from 'next/server'; 3 | 4 | export const GET = async (req: NextRequest) => { 5 | console.log(req.url); 6 | console.log(req.nextUrl.searchParams.get('id')); 7 | 8 | const users = await fetchUsers(); 9 | return Response.json({ users }); 10 | }; 11 | 12 | export const POST = async (req: Request) => { 13 | const user = await req.json(); 14 | const newUser = { ...user, id: Date.now().toString() }; 15 | await saveUser(newUser); 16 | return Response.json({ msg: 'user created' }); 17 | }; 18 | -------------------------------------------------------------------------------- /01-nextjs-tutorial/app/counter/page.tsx: -------------------------------------------------------------------------------- 1 | import Counter from '@/components/Counter'; 2 | 3 | function CounterPage() { 4 | return ( 5 |
6 |

Page Content

7 | 8 |
9 | ); 10 | } 11 | export default CounterPage; 12 | -------------------------------------------------------------------------------- /01-nextjs-tutorial/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/nextjs-course-home-away/201f7acc32b8c794ffaeb31d6e2b1a29dd0c3ca2/01-nextjs-tutorial/app/favicon.ico -------------------------------------------------------------------------------- /01-nextjs-tutorial/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /01-nextjs-tutorial/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import './globals.css'; 2 | import Navbar from '@/components/Navbar'; 3 | import type { Metadata } from 'next'; 4 | import { Inter } from 'next/font/google'; 5 | 6 | const inter = Inter({ subsets: ['latin'] }); 7 | 8 | export const metadata: Metadata = { 9 | title: 'Next.js Project', 10 | description: 'A Next.js project with TypeScript and TailwindCSS.', 11 | keywords: 'Next.js, Typescript, TailwindCSS', 12 | }; 13 | 14 | export default function RootLayout({ 15 | children, 16 | }: { 17 | children: React.ReactNode; 18 | }) { 19 | return ( 20 | 21 | 22 | 23 |
{children}
24 | 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /01-nextjs-tutorial/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | function HomePage() { 4 | return ( 5 |
6 |

Home Page

7 | 8 | about page 9 | 10 |
11 | ); 12 | } 13 | export default HomePage; 14 | -------------------------------------------------------------------------------- /01-nextjs-tutorial/app/tours/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import mapsImg from '@/images/maps.jpg'; 2 | import Image from 'next/image'; 3 | const url = 'https://www.course-api.com/images/tours/tour-1.jpeg'; 4 | 5 | function page({ params }: { params: { id: string } }) { 6 | return ( 7 |
8 |

ID : {params.id}

9 |
10 | {/* local image */} 11 |
12 | maps 20 |

local image

21 |
22 | {/* remote image */} 23 |
24 | tour 32 |

remote image

33 |
34 |
35 |
36 | ); 37 | } 38 | export default page; 39 | -------------------------------------------------------------------------------- /01-nextjs-tutorial/app/tours/error.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | function error({ error }: { error: Error }) { 4 | console.log(error); 5 | 6 | return
there was an error...
; 7 | } 8 | export default error; 9 | -------------------------------------------------------------------------------- /01-nextjs-tutorial/app/tours/layout.tsx: -------------------------------------------------------------------------------- 1 | function ToursLayout({ children }: { children: React.ReactNode }) { 2 | return ( 3 |
4 |
5 |

Nested Layout

6 |
7 | {children} 8 |
9 | ); 10 | } 11 | export default ToursLayout; 12 | -------------------------------------------------------------------------------- /01-nextjs-tutorial/app/tours/loading.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | function loading() { 4 | return loading tours...; 5 | } 6 | export default loading; 7 | -------------------------------------------------------------------------------- /01-nextjs-tutorial/app/tours/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import Image from 'next/image'; 3 | const url = 'https://www.course-api.com/react-tours-project'; 4 | 5 | type Tour = { 6 | id: string; 7 | name: string; 8 | info: string; 9 | image: string; 10 | price: string; 11 | }; 12 | 13 | const fetchTours = async () => { 14 | await new Promise((resolve) => setTimeout(resolve, 3000)); 15 | const response = await fetch(url); 16 | const data: Tour[] = await response.json(); 17 | return data; 18 | }; 19 | 20 | async function ToursPage() { 21 | const data = await fetchTours(); 22 | return ( 23 |
24 |

Tours

25 |
26 | {data.map((tour) => { 27 | return ( 28 | 33 |
34 | {tour.name} 42 |
43 | 44 |

{tour.name}

45 | 46 | ); 47 | })} 48 |
49 |
50 | ); 51 | } 52 | export default ToursPage; 53 | -------------------------------------------------------------------------------- /01-nextjs-tutorial/components/Counter.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState } from 'react'; 4 | 5 | function Counter() { 6 | const [count, setCount] = useState(0); 7 | 8 | return ( 9 |
10 |

{count}

11 | 17 |
18 | ); 19 | } 20 | export default Counter; 21 | -------------------------------------------------------------------------------- /01-nextjs-tutorial/components/DeleteButton.tsx: -------------------------------------------------------------------------------- 1 | import { deleteUser, removeUser } from '@/utils/actions'; 2 | function DeleteButton({ id }: { id: string }) { 3 | const removeUserWithId = removeUser.bind(null, id); 4 | return ( 5 | 6 | 7 | 13 | 14 | ); 15 | } 16 | export default DeleteButton; 17 | -------------------------------------------------------------------------------- /01-nextjs-tutorial/components/Form.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { useFormState, useFormStatus } from 'react-dom'; 3 | 4 | import { createUser } from '@/utils/actions'; 5 | 6 | const SubmitButton = () => { 7 | const { pending } = useFormStatus(); 8 | return ( 9 | 12 | ); 13 | }; 14 | 15 | function Form() { 16 | const [message, formAction] = useFormState(createUser, null); 17 | return ( 18 |
19 | {message &&

{message}

} 20 |

create user

21 | 28 | 35 | 36 | 37 | ); 38 | } 39 | 40 | const formStyle = 'max-w-lg flex flex-col gap-y-4 shadow rounded p-8'; 41 | const inputStyle = 'border shadow rounded py-2 px-3 text-gray-700'; 42 | const btnStyle = 43 | 'bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded capitalize'; 44 | 45 | export default Form; 46 | -------------------------------------------------------------------------------- /01-nextjs-tutorial/components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | function Navbar() { 4 | return ( 5 | 11 | ); 12 | } 13 | export default Navbar; 14 | -------------------------------------------------------------------------------- /01-nextjs-tutorial/components/UsersList.tsx: -------------------------------------------------------------------------------- 1 | import { fetchUsers } from '@/utils/actions'; 2 | import DeleteButton from './DeleteButton'; 3 | async function UsersList() { 4 | const users = await fetchUsers(); 5 | return ( 6 |
7 | {users.length ? ( 8 |
9 | {users.map((user) => { 10 | return ( 11 |

15 | {user.firstName} {user.lastName} 16 | 17 |

18 | ); 19 | })} 20 |
21 | ) : ( 22 |

No users found...

23 | )} 24 |
25 | ); 26 | } 27 | export default UsersList; 28 | -------------------------------------------------------------------------------- /01-nextjs-tutorial/images/maps.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/nextjs-course-home-away/201f7acc32b8c794ffaeb31d6e2b1a29dd0c3ca2/01-nextjs-tutorial/images/maps.jpg -------------------------------------------------------------------------------- /01-nextjs-tutorial/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: 'https', 7 | hostname: 'www.course-api.com', 8 | port: '', 9 | pathname: '/images/**', 10 | }, 11 | ], 12 | }, 13 | }; 14 | 15 | export default nextConfig; 16 | -------------------------------------------------------------------------------- /01-nextjs-tutorial/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tutorial", 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 | "react": "^18", 13 | "react-dom": "^18", 14 | "next": "14.1.4" 15 | }, 16 | "devDependencies": { 17 | "typescript": "^5", 18 | "@types/node": "^20", 19 | "@types/react": "^18", 20 | "@types/react-dom": "^18", 21 | "autoprefixer": "^10.0.1", 22 | "postcss": "^8", 23 | "tailwindcss": "^3.3.0", 24 | "eslint": "^8", 25 | "eslint-config-next": "14.1.4" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /01-nextjs-tutorial/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /01-nextjs-tutorial/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /01-nextjs-tutorial/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /01-nextjs-tutorial/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | content: [ 5 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", 13 | "gradient-conic": 14 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", 15 | }, 16 | }, 17 | }, 18 | plugins: [], 19 | }; 20 | export default config; 21 | -------------------------------------------------------------------------------- /01-nextjs-tutorial/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 | -------------------------------------------------------------------------------- /01-nextjs-tutorial/users.json: -------------------------------------------------------------------------------- 1 | [{"firstName":"peter","lastName":"smith","id":"1713023562290"}] -------------------------------------------------------------------------------- /01-nextjs-tutorial/utils/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | import { readFile, writeFile } from 'fs/promises'; 3 | import { revalidatePath } from 'next/cache'; 4 | import { redirect } from 'next/navigation'; 5 | 6 | type User = { 7 | id: string; 8 | firstName: string; 9 | lastName: string; 10 | }; 11 | 12 | export const createUser = async (prevState: any, formData: FormData) => { 13 | 'use server'; 14 | // console.log(prevState); 15 | 16 | await new Promise((resolve) => setTimeout(resolve, 3000)); 17 | const firstName = formData.get('firstName') as string; 18 | const lastName = formData.get('lastName') as string; 19 | const newUser: User = { firstName, lastName, id: Date.now().toString() }; 20 | 21 | try { 22 | await saveUser(newUser); 23 | revalidatePath('/actions'); 24 | 25 | // some logic 26 | return 'user created successfully...'; 27 | } catch (error) { 28 | console.log(error); 29 | return 'failed to create user...'; 30 | } 31 | }; 32 | 33 | export const fetchUsers = async (): Promise => { 34 | const result = await readFile('users.json', { encoding: 'utf8' }); 35 | const users = result ? JSON.parse(result) : []; 36 | return users; 37 | }; 38 | 39 | export const saveUser = async (user: User) => { 40 | const users = await fetchUsers(); 41 | users.push(user); 42 | await writeFile('users.json', JSON.stringify(users)); 43 | }; 44 | 45 | export const deleteUser = async (formData: FormData) => { 46 | const id = formData.get('id') as string; 47 | const users = await fetchUsers(); 48 | const updatedUsers = users.filter((user) => user.id !== id); 49 | await writeFile('users.json', JSON.stringify(updatedUsers)); 50 | revalidatePath('/actions'); 51 | }; 52 | export const removeUser = async (id: string, formData: FormData) => { 53 | const name = formData.get('name') as string; 54 | // console.log(name); 55 | 56 | const users = await fetchUsers(); 57 | const updatedUsers = users.filter((user) => user.id !== id); 58 | await writeFile('users.json', JSON.stringify(updatedUsers)); 59 | revalidatePath('/actions'); 60 | }; 61 | -------------------------------------------------------------------------------- /02-home-away-project/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /02-home-away-project/.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 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /02-home-away-project/app/admin/page.tsx: -------------------------------------------------------------------------------- 1 | import ChartsContainer from '@/components/admin/ChartsContainer'; 2 | import { 3 | ChartsLoadingContainer, 4 | StatsLoadingContainer, 5 | } from '@/components/admin/Loading'; 6 | import StatsContainer from '@/components/admin/StatsContainer'; 7 | import { Suspense } from 'react'; 8 | 9 | function AdminPage() { 10 | return ( 11 | <> 12 | }> 13 | 14 | 15 | }> 16 | 17 | 18 | 19 | ); 20 | } 21 | export default AdminPage; 22 | -------------------------------------------------------------------------------- /02-home-away-project/app/api/confirm/route.ts: -------------------------------------------------------------------------------- 1 | import Stripe from 'stripe'; 2 | const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string); 3 | import { redirect } from 'next/navigation'; 4 | 5 | import { NextResponse, type NextRequest } from 'next/server'; 6 | import db from '@/utils/db'; 7 | 8 | export const GET = async (req: NextRequest) => { 9 | const { searchParams } = new URL(req.url); 10 | const session_id = searchParams.get('session_id') as string; 11 | 12 | try { 13 | const session = await stripe.checkout.sessions.retrieve(session_id); 14 | const bookingId = session.metadata?.bookingId; 15 | if (session.status !== 'complete' || !bookingId) { 16 | throw new Error('Something went wrong'); 17 | } 18 | await db.booking.update({ 19 | where: { id: bookingId }, 20 | data: { paymentStatus: true }, 21 | }); 22 | } catch (error) { 23 | console.log(error); 24 | return NextResponse.json(null, { 25 | status: 500, 26 | statusText: 'Internal Server Error', 27 | }); 28 | } 29 | redirect('/bookings'); 30 | }; 31 | -------------------------------------------------------------------------------- /02-home-away-project/app/api/payment/route.ts: -------------------------------------------------------------------------------- 1 | import Stripe from 'stripe'; 2 | const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string); 3 | import { type NextRequest, type NextResponse } from 'next/server'; 4 | import db from '@/utils/db'; 5 | import { formatDate } from '@/utils/format'; 6 | 7 | export const POST = async (req: NextRequest, res: NextResponse) => { 8 | const requestHeaders = new Headers(req.headers); 9 | const origin = requestHeaders.get('origin'); 10 | const { bookingId } = await req.json(); 11 | 12 | const booking = await db.booking.findUnique({ 13 | where: { id: bookingId }, 14 | include: { 15 | property: { 16 | select: { 17 | name: true, 18 | image: true, 19 | }, 20 | }, 21 | }, 22 | }); 23 | if (!booking) { 24 | return Response.json(null, { 25 | status: 404, 26 | statusText: 'Not Found', 27 | }); 28 | } 29 | const { 30 | totalNights, 31 | orderTotal, 32 | checkIn, 33 | checkOut, 34 | property: { image, name }, 35 | } = booking; 36 | 37 | try { 38 | const session = await stripe.checkout.sessions.create({ 39 | ui_mode: 'embedded', 40 | metadata: { bookingId: booking.id }, 41 | line_items: [ 42 | { 43 | quantity: 1, 44 | price_data: { 45 | currency: 'usd', 46 | product_data: { 47 | name: `${name}`, 48 | images: [image], 49 | description: `Stay in this wonderful place for ${totalNights} nights, from ${formatDate( 50 | checkIn 51 | )} to ${formatDate(checkOut)}. Enjoy your stay!`, 52 | }, 53 | unit_amount: orderTotal * 100, 54 | }, 55 | }, 56 | ], 57 | mode: 'payment', 58 | return_url: `${origin}/api/confirm?session_id={CHECKOUT_SESSION_ID}`, 59 | }); 60 | return Response.json({ clientSecret: session.client_secret }); 61 | } catch (error) { 62 | console.log(error); 63 | return Response.json(null, { 64 | status: 500, 65 | statusText: 'Internal Server Error', 66 | }); 67 | } 68 | }; 69 | -------------------------------------------------------------------------------- /02-home-away-project/app/bookings/loading.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import LoadingTable from '@/components/booking/LoadingTable'; 3 | function loading() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | 11 | export default loading; 12 | -------------------------------------------------------------------------------- /02-home-away-project/app/bookings/page.tsx: -------------------------------------------------------------------------------- 1 | import EmptyList from '@/components/home/EmptyList'; 2 | import CountryFlagAndName from '@/components/card/CountryFlagAndName'; 3 | import Link from 'next/link'; 4 | 5 | import { formatDate, formatCurrency } from '@/utils/format'; 6 | import { 7 | Table, 8 | TableBody, 9 | TableCaption, 10 | TableCell, 11 | TableHead, 12 | TableHeader, 13 | TableRow, 14 | } from '@/components/ui/table'; 15 | 16 | import FormContainer from '@/components/form/FormContainer'; 17 | import { IconButton } from '@/components/form/Buttons'; 18 | import { fetchBookings } from '@/utils/actions'; 19 | import { deleteBookingAction } from '@/utils/actions'; 20 | import LoadingTable from '@/components/booking/LoadingTable'; 21 | 22 | async function BookingsPage() { 23 | const bookings = await fetchBookings(); 24 | if (bookings.length === 0) { 25 | return ; 26 | } 27 | return ( 28 |
29 |

total bookings : {bookings.length}

30 | 31 | A list of your recent bookings. 32 | 33 | 34 | Property Name 35 | Country 36 | Nights 37 | Total 38 | Check In 39 | Check Out 40 | Actions 41 | 42 | 43 | 44 | {bookings.map((booking) => { 45 | const { id, orderTotal, totalNights, checkIn, checkOut } = booking; 46 | const { id: propertyId, name, country } = booking.property; 47 | const startDate = formatDate(checkIn); 48 | const endDate = formatDate(checkOut); 49 | return ( 50 | 51 | 52 | 56 | {name} 57 | 58 | 59 | 60 | 61 | 62 | {totalNights} 63 | {formatCurrency(orderTotal)} 64 | {startDate} 65 | {endDate} 66 | 67 | 68 | 69 | 70 | ); 71 | })} 72 | 73 |
74 |
75 | ); 76 | } 77 | 78 | function DeleteBooking({ bookingId }: { bookingId: string }) { 79 | const deleteBooking = deleteBookingAction.bind(null, { bookingId }); 80 | return ( 81 | 82 | 83 | 84 | ); 85 | } 86 | 87 | export default BookingsPage; 88 | -------------------------------------------------------------------------------- /02-home-away-project/app/checkout/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import axios from 'axios'; 3 | import { useSearchParams } from 'next/navigation'; 4 | import React, { useCallback } from 'react'; 5 | import { loadStripe } from '@stripe/stripe-js'; 6 | import { 7 | EmbeddedCheckoutProvider, 8 | EmbeddedCheckout, 9 | } from '@stripe/react-stripe-js'; 10 | 11 | const stripePromise = loadStripe( 12 | process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY as string 13 | ); 14 | 15 | function CheckoutPage() { 16 | const searchParams = useSearchParams(); 17 | const bookingId = searchParams.get('bookingId'); 18 | 19 | const fetchClientSecret = useCallback(async () => { 20 | const response = await axios.post('/api/payment', { 21 | bookingId: bookingId, 22 | }); 23 | return response.data.clientSecret; 24 | }, []); 25 | 26 | const options = { fetchClientSecret }; 27 | 28 | return ( 29 |
30 | 31 | 32 | 33 |
34 | ); 35 | } 36 | export default CheckoutPage; 37 | -------------------------------------------------------------------------------- /02-home-away-project/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/nextjs-course-home-away/201f7acc32b8c794ffaeb31d6e2b1a29dd0c3ca2/02-home-away-project/app/favicon.ico -------------------------------------------------------------------------------- /02-home-away-project/app/favorites/loading.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import LoadingCards from '@/components/card/LoadingCards'; 4 | 5 | function loading() { 6 | return ; 7 | } 8 | export default loading; 9 | -------------------------------------------------------------------------------- /02-home-away-project/app/favorites/page.tsx: -------------------------------------------------------------------------------- 1 | import EmptyList from '@/components/home/EmptyList'; 2 | import PropertiesList from '@/components/home/PropertiesList'; 3 | import { fetchFavorites } from '@/utils/actions'; 4 | 5 | async function FavoritesPage() { 6 | const favorites = await fetchFavorites(); 7 | 8 | if (favorites.length === 0) { 9 | return ; 10 | } 11 | 12 | return ; 13 | } 14 | export default FavoritesPage; 15 | -------------------------------------------------------------------------------- /02-home-away-project/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer components { 6 | .container { 7 | @apply mx-auto max-w-6xl xl:max-w-7xl px-8; 8 | } 9 | } 10 | 11 | @layer base { 12 | :root { 13 | --background: 0 0% 100%; 14 | --foreground: 20 14.3% 4.1%; 15 | --card: 0 0% 100%; 16 | --card-foreground: 20 14.3% 4.1%; 17 | --popover: 0 0% 100%; 18 | --popover-foreground: 20 14.3% 4.1%; 19 | --primary: 24.6 95% 53.1%; 20 | --primary-foreground: 60 9.1% 97.8%; 21 | --secondary: 60 4.8% 95.9%; 22 | --secondary-foreground: 24 9.8% 10%; 23 | --muted: 60 4.8% 95.9%; 24 | --muted-foreground: 25 5.3% 44.7%; 25 | --accent: 60 4.8% 95.9%; 26 | --accent-foreground: 24 9.8% 10%; 27 | --destructive: 0 84.2% 60.2%; 28 | --destructive-foreground: 60 9.1% 97.8%; 29 | --border: 20 5.9% 90%; 30 | --input: 20 5.9% 90%; 31 | --ring: 24.6 95% 53.1%; 32 | --radius: 0.5rem; 33 | } 34 | 35 | .dark { 36 | --background: 20 14.3% 4.1%; 37 | --foreground: 60 9.1% 97.8%; 38 | --card: 20 14.3% 4.1%; 39 | --card-foreground: 60 9.1% 97.8%; 40 | --popover: 20 14.3% 4.1%; 41 | --popover-foreground: 60 9.1% 97.8%; 42 | --primary: 20.5 90.2% 48.2%; 43 | --primary-foreground: 60 9.1% 97.8%; 44 | --secondary: 12 6.5% 15.1%; 45 | --secondary-foreground: 60 9.1% 97.8%; 46 | --muted: 12 6.5% 15.1%; 47 | --muted-foreground: 24 5.4% 63.9%; 48 | --accent: 12 6.5% 15.1%; 49 | --accent-foreground: 60 9.1% 97.8%; 50 | --destructive: 0 72.2% 50.6%; 51 | --destructive-foreground: 60 9.1% 97.8%; 52 | --border: 12 6.5% 15.1%; 53 | --input: 12 6.5% 15.1%; 54 | --ring: 20.5 90.2% 48.2%; 55 | } 56 | } 57 | 58 | @layer base { 59 | * { 60 | @apply border-border; 61 | } 62 | body { 63 | @apply bg-background text-foreground; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /02-home-away-project/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | import { Inter } from 'next/font/google'; 3 | import './globals.css'; 4 | import Navbar from '@/components/navbar/Navbar'; 5 | import Providers from './providers'; 6 | import { ClerkProvider } from '@clerk/nextjs'; 7 | const inter = Inter({ subsets: ['latin'] }); 8 | 9 | export const metadata: Metadata = { 10 | title: 'HomeAway Draft', 11 | description: 'Feel at home, away from home.', 12 | }; 13 | 14 | export default function RootLayout({ 15 | children, 16 | }: Readonly<{ 17 | children: React.ReactNode; 18 | }>) { 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 |
{children}
26 |
27 | 28 | 29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /02-home-away-project/app/page.tsx: -------------------------------------------------------------------------------- 1 | import LoadingCards from '@/components/card/LoadingCards'; 2 | import CategoriesList from '@/components/home/CategoriesList'; 3 | import PropertiesContainer from '@/components/home/PropertiesContainer'; 4 | import { Suspense } from 'react'; 5 | function HomePage({ 6 | searchParams, 7 | }: { 8 | searchParams: { category?: string; search?: string }; 9 | }) { 10 | return ( 11 |
12 | 16 | }> 17 | 21 | 22 |
23 | ); 24 | } 25 | export default HomePage; 26 | -------------------------------------------------------------------------------- /02-home-away-project/app/profile/create/page.tsx: -------------------------------------------------------------------------------- 1 | import { SubmitButton } from '@/components/form/Buttons'; 2 | import FormContainer from '@/components/form/FormContainer'; 3 | import FormInput from '@/components/form/FormInput'; 4 | import { createProfileAction } from '@/utils/actions'; 5 | import { currentUser } from '@clerk/nextjs/server'; 6 | import { redirect } from 'next/navigation'; 7 | async function CreateProfilePage() { 8 | const user = await currentUser(); 9 | 10 | if (user?.privateMetadata?.hasProfile) redirect('/'); 11 | return ( 12 |
13 |

new user

14 |
15 | 16 |
17 | 18 | 19 | 20 |
21 | 22 |
23 |
24 |
25 | ); 26 | } 27 | export default CreateProfilePage; 28 | -------------------------------------------------------------------------------- /02-home-away-project/app/profile/page.tsx: -------------------------------------------------------------------------------- 1 | import FormContainer from '@/components/form/FormContainer'; 2 | import { 3 | updateProfileAction, 4 | fetchProfile, 5 | updateProfileImageAction, 6 | } from '@/utils/actions'; 7 | import FormInput from '@/components/form/FormInput'; 8 | import { SubmitButton } from '@/components/form/Buttons'; 9 | import ImageInputContainer from '@/components/form/ImageInputContainer'; 10 | async function ProfilePage() { 11 | const profile = await fetchProfile(); 12 | 13 | return ( 14 |
15 |

user profile

16 |
17 | 23 | 24 |
25 | 31 | 37 | 43 |
44 | 45 |
46 |
47 |
48 | ); 49 | } 50 | export default ProfilePage; 51 | -------------------------------------------------------------------------------- /02-home-away-project/app/properties/loading.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { Skeleton } from '@/components/ui/skeleton'; 3 | 4 | function loading() { 5 | return ; 6 | } 7 | export default loading; 8 | -------------------------------------------------------------------------------- /02-home-away-project/app/providers.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { Toaster } from '@/components/ui/toaster'; 3 | import { ThemeProvider } from './theme-provider'; 4 | 5 | function Providers({ children }: { children: React.ReactNode }) { 6 | return ( 7 | <> 8 | 9 | 15 | {children} 16 | 17 | 18 | ); 19 | } 20 | export default Providers; 21 | -------------------------------------------------------------------------------- /02-home-away-project/app/rentals/[id]/edit/page.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | fetchRentalDetails, 3 | updatePropertyImageAction, 4 | updatePropertyAction, 5 | } from '@/utils/actions'; 6 | import FormContainer from '@/components/form/FormContainer'; 7 | import FormInput from '@/components/form/FormInput'; 8 | import CategoriesInput from '@/components/form/CategoriesInput'; 9 | import PriceInput from '@/components/form/PriceInput'; 10 | import TextAreaInput from '@/components/form/TextAreaInput'; 11 | import CountriesInput from '@/components/form/CountriesInput'; 12 | import CounterInput from '@/components/form/CounterInput'; 13 | import AmenitiesInput from '@/components/form/AmenitiesInput'; 14 | import { SubmitButton } from '@/components/form/Buttons'; 15 | import { redirect } from 'next/navigation'; 16 | import { type Amenity } from '@/utils/amenities'; 17 | import ImageInputContainer from '@/components/form/ImageInputContainer'; 18 | 19 | async function EditRentalPage({ params }: { params: { id: string } }) { 20 | const property = await fetchRentalDetails(params.id); 21 | 22 | if (!property) redirect('/'); 23 | 24 | const defaultAmenities: Amenity[] = JSON.parse(property.amenities); 25 | 26 | return ( 27 |
28 |

Edit Property

29 |
30 | 36 | 37 | 38 | 39 | 40 | 41 |
42 | 48 | 54 | 55 | 56 | 57 |
58 | 59 | 64 | 65 |

66 | Accommodation Details 67 |

68 | 69 | 70 | 71 | 72 |

Amenities

73 | 74 | 75 |
76 |
77 |
78 | ); 79 | } 80 | export default EditRentalPage; 81 | -------------------------------------------------------------------------------- /02-home-away-project/app/rentals/create/page.tsx: -------------------------------------------------------------------------------- 1 | import FormInput from '@/components/form/FormInput'; 2 | import FormContainer from '@/components/form/FormContainer'; 3 | import { createPropertyAction } from '@/utils/actions'; 4 | import { SubmitButton } from '@/components/form/Buttons'; 5 | import PriceInput from '@/components/form/PriceInput'; 6 | import CategoriesInput from '@/components/form/CategoriesInput'; 7 | import TextAreaInput from '@/components/form/TextAreaInput'; 8 | import CountriesInput from '@/components/form/CountriesInput'; 9 | import ImageInput from '@/components/form/ImageInput'; 10 | import CounterInput from '@/components/form/CounterInput'; 11 | import AmenitiesInput from '@/components/form/AmenitiesInput'; 12 | function CreatePropertyPage() { 13 | return ( 14 |
15 |

16 | create property 17 |

18 |
19 |

General Info

20 | 21 |
22 | 28 | 34 | {/* price */} 35 | 36 | {/* categories */} 37 | 38 |
39 | {/* text area / description */} 40 | 44 |
45 | 46 | 47 |
48 |

49 | Accommodation Details 50 |

51 | 52 | 53 | 54 | 55 |

Amenities

56 | 57 | 58 |
59 |
60 |
61 | ); 62 | } 63 | export default CreatePropertyPage; 64 | -------------------------------------------------------------------------------- /02-home-away-project/app/rentals/loading.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import LoadingTable from '@/components/booking/LoadingTable'; 3 | function loading() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | export default loading; 11 | -------------------------------------------------------------------------------- /02-home-away-project/app/rentals/page.tsx: -------------------------------------------------------------------------------- 1 | import EmptyList from '@/components/home/EmptyList'; 2 | import { fetchRentals, deleteRentalAction } from '@/utils/actions'; 3 | import Link from 'next/link'; 4 | 5 | import { formatCurrency } from '@/utils/format'; 6 | import { 7 | Table, 8 | TableBody, 9 | TableCaption, 10 | TableCell, 11 | TableHead, 12 | TableHeader, 13 | TableRow, 14 | } from '@/components/ui/table'; 15 | 16 | import FormContainer from '@/components/form/FormContainer'; 17 | import { IconButton } from '@/components/form/Buttons'; 18 | 19 | async function RentalsPage() { 20 | const rentals = await fetchRentals(); 21 | 22 | if (rentals.length === 0) { 23 | return ( 24 | 28 | ); 29 | } 30 | 31 | return ( 32 |
33 |

Active Properties : {rentals.length}

34 | 35 | A list of all your properties. 36 | 37 | 38 | Property Name 39 | Nightly Rate 40 | Nights Booked 41 | Total Income 42 | Actions 43 | 44 | 45 | 46 | {rentals.map((rental) => { 47 | const { id: propertyId, name, price } = rental; 48 | const { totalNightsSum, orderTotalSum } = rental; 49 | return ( 50 | 51 | 52 | 56 | {name} 57 | 58 | 59 | {formatCurrency(price)} 60 | {totalNightsSum || 0} 61 | {formatCurrency(orderTotalSum)} 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | ); 71 | })} 72 | 73 |
74 |
75 | ); 76 | } 77 | 78 | function DeleteRental({ propertyId }: { propertyId: string }) { 79 | const deleteRental = deleteRentalAction.bind(null, { propertyId }); 80 | return ( 81 | 82 | 83 | 84 | ); 85 | } 86 | 87 | export default RentalsPage; 88 | -------------------------------------------------------------------------------- /02-home-away-project/app/reservations/loading.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import LoadingTable from '@/components/booking/LoadingTable'; 4 | 5 | function loading() { 6 | return ; 7 | } 8 | export default loading; 9 | -------------------------------------------------------------------------------- /02-home-away-project/app/reservations/page.tsx: -------------------------------------------------------------------------------- 1 | import { fetchReservations } from '@/utils/actions'; 2 | import Link from 'next/link'; 3 | import EmptyList from '@/components/home/EmptyList'; 4 | import CountryFlagAndName from '@/components/card/CountryFlagAndName'; 5 | 6 | import { formatDate, formatCurrency } from '@/utils/format'; 7 | import { 8 | Table, 9 | TableBody, 10 | TableCaption, 11 | TableCell, 12 | TableHead, 13 | TableHeader, 14 | TableRow, 15 | } from '@/components/ui/table'; 16 | import Stats from '@/components/reservations/Stats'; 17 | async function ReservationsPage() { 18 | const reservations = await fetchReservations(); 19 | if (reservations.length === 0) return ; 20 | 21 | return ( 22 | <> 23 | 24 |
25 |

26 | total reservations : {reservations.length} 27 |

28 | 29 | A list of recent reservations 30 | 31 | 32 | Property Name 33 | Country 34 | Nights 35 | Total 36 | Check In 37 | Check Out 38 | 39 | 40 | 41 | {reservations.map((item) => { 42 | const { id, orderTotal, totalNights, checkIn, checkOut } = item; 43 | const { id: propertyId, name, country } = item.property; 44 | 45 | const startDate = formatDate(checkIn); 46 | const endDate = formatDate(checkOut); 47 | return ( 48 | 49 | 50 | 54 | {name} 55 | 56 | 57 | 58 | 59 | 60 | {totalNights} 61 | {formatCurrency(orderTotal)} 62 | {startDate} 63 | {endDate} 64 | 65 | ); 66 | })} 67 | 68 |
69 |
70 | 71 | ); 72 | } 73 | export default ReservationsPage; 74 | -------------------------------------------------------------------------------- /02-home-away-project/app/reviews/loading.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Card, CardContent, CardHeader } from '@/components/ui/card'; 4 | import { Skeleton } from '@/components/ui/skeleton'; 5 | function loading() { 6 | return ( 7 |
8 | 9 | 10 |
11 | ); 12 | } 13 | 14 | const ReviewLoadingCard = () => { 15 | return ( 16 | 17 | 18 |
19 | 20 |
21 | 22 | 23 |
24 |
25 |
26 |
27 | ); 28 | }; 29 | 30 | export default loading; 31 | -------------------------------------------------------------------------------- /02-home-away-project/app/reviews/page.tsx: -------------------------------------------------------------------------------- 1 | import EmptyList from '@/components/home/EmptyList'; 2 | import { 3 | deleteReviewAction, 4 | fetchPropertyReviewsByUser, 5 | } from '@/utils/actions'; 6 | import ReviewCard from '@/components/reviews/ReviewCard'; 7 | import Title from '@/components/properties/Title'; 8 | import FormContainer from '@/components/form/FormContainer'; 9 | import { IconButton } from '@/components/form/Buttons'; 10 | async function ReviewsPage() { 11 | const reviews = await fetchPropertyReviewsByUser(); 12 | if (reviews.length === 0) return ; 13 | 14 | return ( 15 | <> 16 | 17 | <section className='grid md:grid-cols-2 gap-8 mt-4 '> 18 | {reviews.map((review) => { 19 | const { comment, rating } = review; 20 | const { name, image } = review.property; 21 | const reviewInfo = { 22 | comment, 23 | rating, 24 | name, 25 | image, 26 | }; 27 | return ( 28 | <ReviewCard key={review.id} reviewInfo={reviewInfo}> 29 | <DeleteReview reviewId={review.id} /> 30 | </ReviewCard> 31 | ); 32 | })} 33 | </section> 34 | </> 35 | ); 36 | } 37 | 38 | const DeleteReview = ({ reviewId }: { reviewId: string }) => { 39 | const deleteReview = deleteReviewAction.bind(null, { reviewId }); 40 | return ( 41 | <FormContainer action={deleteReview}> 42 | <IconButton actionType='delete' /> 43 | </FormContainer> 44 | ); 45 | }; 46 | 47 | export default ReviewsPage; 48 | -------------------------------------------------------------------------------- /02-home-away-project/app/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import { ThemeProvider as NextThemesProvider } from 'next-themes'; 5 | import { type ThemeProviderProps } from 'next-themes/dist/types'; 6 | 7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 8 | return <NextThemesProvider {...props}>{children}</NextThemesProvider>; 9 | } 10 | -------------------------------------------------------------------------------- /02-home-away-project/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": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /02-home-away-project/components/admin/Chart.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { 3 | BarChart, 4 | Bar, 5 | XAxis, 6 | YAxis, 7 | CartesianGrid, 8 | Tooltip, 9 | ResponsiveContainer, 10 | } from 'recharts'; 11 | 12 | type ChartPropsType = { 13 | data: { 14 | date: string; 15 | count: number; 16 | }[]; 17 | }; 18 | 19 | function Chart({ data }: ChartPropsType) { 20 | return ( 21 | <section className='mt-24'> 22 | <h1 className='text-4xl font-semibold text-center'>Monthly Bookings</h1> 23 | <ResponsiveContainer width='100%' height={300}> 24 | <BarChart data={data} margin={{ top: 50 }}> 25 | <CartesianGrid strokeDasharray='3 3' /> 26 | <XAxis dataKey='date' /> 27 | <YAxis allowDecimals={false} /> 28 | <Tooltip /> 29 | <Bar dataKey='count' fill='#f97215' barSize={75} /> 30 | </BarChart> 31 | </ResponsiveContainer> 32 | </section> 33 | ); 34 | } 35 | export default Chart; 36 | -------------------------------------------------------------------------------- /02-home-away-project/components/admin/ChartsContainer.tsx: -------------------------------------------------------------------------------- 1 | import { fetchChartsData } from '@/utils/actions'; 2 | import Chart from './Chart'; 3 | 4 | async function ChartsContainer() { 5 | const bookings = await fetchChartsData(); 6 | if (bookings.length < 1) return null; 7 | return <Chart data={bookings} />; 8 | } 9 | export default ChartsContainer; 10 | -------------------------------------------------------------------------------- /02-home-away-project/components/admin/Loading.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardHeader } from '../ui/card'; 2 | import { Skeleton } from '../ui/skeleton'; 3 | 4 | export function StatsLoadingContainer() { 5 | return ( 6 | <div className='mt-8 grid md:grid-cols-2 gap-4 lg:grid-cols-3'> 7 | <LoadingCard /> 8 | <LoadingCard /> 9 | <LoadingCard /> 10 | </div> 11 | ); 12 | } 13 | 14 | function LoadingCard() { 15 | return ( 16 | <Card> 17 | <CardHeader> 18 | <Skeleton className='w-full h-20 rounded' /> 19 | </CardHeader> 20 | </Card> 21 | ); 22 | } 23 | 24 | export function ChartsLoadingContainer() { 25 | return <Skeleton className='mt-16 w-full h-[300px] rounded' />; 26 | } 27 | -------------------------------------------------------------------------------- /02-home-away-project/components/admin/StatsCard.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardHeader } from '../ui/card'; 2 | type StatsCardProps = { 3 | title: string; 4 | value: number | string; 5 | }; 6 | 7 | function StatsCard({ title, value }: StatsCardProps) { 8 | return ( 9 | <Card className='bg-muted'> 10 | <CardHeader className='flex flex-row justify-between items-center'> 11 | <h3 className='capitalize text-3xl font-bold'>{title}</h3> 12 | <span className='text-primary text-5xl font-extrabold'>{value}</span> 13 | </CardHeader> 14 | </Card> 15 | ); 16 | } 17 | export default StatsCard; 18 | -------------------------------------------------------------------------------- /02-home-away-project/components/admin/StatsContainer.tsx: -------------------------------------------------------------------------------- 1 | import { fetchStats } from '@/utils/actions'; 2 | import StatsCard from './StatsCard'; 3 | 4 | async function StatsContainer() { 5 | const data = await fetchStats(); 6 | 7 | return ( 8 | <div className='mt-8 grid md:grid-cols-2 gap-4 lg:grid-cols-3'> 9 | <StatsCard title='users' value={data.usersCount || 0} /> 10 | <StatsCard title='properties' value={data.propertiesCount || 0} /> 11 | <StatsCard title='bookings' value={data.bookingsCount || 0} /> 12 | </div> 13 | ); 14 | } 15 | export default StatsContainer; 16 | -------------------------------------------------------------------------------- /02-home-away-project/components/booking/BookingCalendar.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { Calendar } from '@/components/ui/calendar'; 3 | import { useEffect, useState } from 'react'; 4 | import { useToast } from '@/components/ui/use-toast'; 5 | import { DateRange } from 'react-day-picker'; 6 | import { useProperty } from '@/utils/store'; 7 | 8 | import { 9 | generateDisabledDates, 10 | generateDateRange, 11 | defaultSelected, 12 | generateBlockedPeriods, 13 | } from '@/utils/calendar'; 14 | 15 | function BookingCalendar() { 16 | const currentDate = new Date(); 17 | 18 | const [range, setRange] = useState<DateRange | undefined>(defaultSelected); 19 | 20 | const bookings = useProperty((state) => state.bookings); 21 | const blockedPeriods = generateBlockedPeriods({ 22 | bookings, 23 | today: currentDate, 24 | }); 25 | const { toast } = useToast(); 26 | const unavailableDates = generateDisabledDates(blockedPeriods); 27 | 28 | useEffect(() => { 29 | const selectedRange = generateDateRange(range); 30 | const isDisabledDateIncluded = selectedRange.some((date) => { 31 | if (unavailableDates[date]) { 32 | setRange(defaultSelected); 33 | toast({ 34 | description: 'Some dates are booked. Please select again.', 35 | }); 36 | return true; 37 | } 38 | return false; 39 | }); 40 | useProperty.setState({ range }); 41 | }, [range]); 42 | 43 | return ( 44 | <Calendar 45 | mode='range' 46 | defaultMonth={currentDate} 47 | selected={range} 48 | onSelect={setRange} 49 | className='mb-4' 50 | // add disabled 51 | disabled={blockedPeriods} 52 | /> 53 | ); 54 | } 55 | export default BookingCalendar; 56 | -------------------------------------------------------------------------------- /02-home-away-project/components/booking/BookingContainer.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useProperty } from '@/utils/store'; 4 | import ConfirmBooking from './ConfirmBooking'; 5 | import BookingForm from './BookingForm'; 6 | function BookingContainer() { 7 | const { range } = useProperty((state) => state); 8 | 9 | if (!range || !range.from || !range.to) return null; 10 | if (range.to.getTime() === range.from.getTime()) return null; 11 | return ( 12 | <div className='w-full'> 13 | <BookingForm /> 14 | <ConfirmBooking /> 15 | </div> 16 | ); 17 | } 18 | 19 | export default BookingContainer; 20 | -------------------------------------------------------------------------------- /02-home-away-project/components/booking/BookingForm.tsx: -------------------------------------------------------------------------------- 1 | import { calculateTotals } from '@/utils/calculateTotals'; 2 | import { Card, CardTitle } from '@/components/ui/card'; 3 | import { Separator } from '@/components/ui/separator'; 4 | import { useProperty } from '@/utils/store'; 5 | import { formatCurrency } from '@/utils/format'; 6 | function BookingForm() { 7 | const { range, price } = useProperty((state) => state); 8 | const checkIn = range?.from as Date; 9 | const checkOut = range?.to as Date; 10 | 11 | const { totalNights, subTotal, cleaning, service, tax, orderTotal } = 12 | calculateTotals({ 13 | checkIn, 14 | checkOut, 15 | price, 16 | }); 17 | return ( 18 | <Card className='p-8 mb-4'> 19 | <CardTitle className='mb-8'>Summary </CardTitle> 20 | <FormRow label={`$${price} x ${totalNights} nights`} amount={subTotal} /> 21 | <FormRow label='Cleaning Fee' amount={cleaning} /> 22 | <FormRow label='Service Fee' amount={service} /> 23 | <FormRow label='Tax' amount={tax} /> 24 | <Separator className='mt-4' /> 25 | <CardTitle className='mt-8'> 26 | <FormRow label='Booking Total' amount={orderTotal} /> 27 | </CardTitle> 28 | </Card> 29 | ); 30 | } 31 | 32 | function FormRow({ label, amount }: { label: string; amount: number }) { 33 | return ( 34 | <p className='flex justify-between text-sm mb-2'> 35 | <span>{label}</span> 36 | <span>{formatCurrency(amount)}</span> 37 | </p> 38 | ); 39 | } 40 | 41 | export default BookingForm; 42 | -------------------------------------------------------------------------------- /02-home-away-project/components/booking/BookingWrapper.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useProperty } from '@/utils/store'; 4 | import { Booking } from '@/utils/types'; 5 | import BookingCalendar from './BookingCalendar'; 6 | import BookingContainer from './BookingContainer'; 7 | import { useEffect } from 'react'; 8 | 9 | type BookingWrapperProps = { 10 | propertyId: string; 11 | price: number; 12 | bookings: Booking[]; 13 | }; 14 | export default function BookingWrapper({ 15 | propertyId, 16 | price, 17 | bookings, 18 | }: BookingWrapperProps) { 19 | useEffect(() => { 20 | useProperty.setState({ 21 | propertyId, 22 | price, 23 | bookings, 24 | }); 25 | }, []); 26 | return ( 27 | <> 28 | <BookingCalendar /> 29 | <BookingContainer /> 30 | </> 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /02-home-away-project/components/booking/ConfirmBooking.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { SignInButton, useAuth } from '@clerk/nextjs'; 3 | import { Button } from '@/components/ui/button'; 4 | import { useProperty } from '@/utils/store'; 5 | import FormContainer from '@/components/form/FormContainer'; 6 | import { SubmitButton } from '@/components/form/Buttons'; 7 | import { createBookingAction } from '@/utils/actions'; 8 | 9 | function ConfirmBooking() { 10 | const { userId } = useAuth(); 11 | const { propertyId, range } = useProperty((state) => state); 12 | const checkIn = range?.from as Date; 13 | const checkOut = range?.to as Date; 14 | if (!userId) 15 | return ( 16 | <SignInButton mode='modal'> 17 | <Button type='button' className='w-full'> 18 | Sign In to Complete Booking 19 | </Button> 20 | </SignInButton> 21 | ); 22 | 23 | const createBooking = createBookingAction.bind(null, { 24 | propertyId, 25 | checkIn, 26 | checkOut, 27 | }); 28 | return ( 29 | <section> 30 | <FormContainer action={createBooking}> 31 | <SubmitButton text='Reserve' className='w-full' /> 32 | </FormContainer> 33 | </section> 34 | ); 35 | } 36 | export default ConfirmBooking; 37 | -------------------------------------------------------------------------------- /02-home-away-project/components/booking/LoadingTable.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from '@/components/ui/skeleton'; 2 | import { Separator } from '@radix-ui/react-dropdown-menu'; 3 | 4 | function LoadingTable({ rows }: { rows?: number }) { 5 | const tableRows = Array.from({ length: rows || 5 }, (_, i) => { 6 | return ( 7 | <div className='mb-4' key={i}> 8 | <Skeleton className='w-full h-8 rounded' /> 9 | <Separator /> 10 | </div> 11 | ); 12 | }); 13 | return <>{tableRows}</>; 14 | } 15 | export default LoadingTable; 16 | -------------------------------------------------------------------------------- /02-home-away-project/components/card/CountryFlagAndName.tsx: -------------------------------------------------------------------------------- 1 | import { findCountryByCode } from '@/utils/countries'; 2 | 3 | function CountryFlagAndName({ countryCode }: { countryCode: string }) { 4 | const validCountry = findCountryByCode(countryCode)!; 5 | 6 | const countryName = 7 | validCountry.name.length > 20 8 | ? `${validCountry.name.substring(0, 20)}...` 9 | : validCountry.name; 10 | return ( 11 | <span className='flex justify-between items-center gap-2 text-sm'> 12 | {validCountry.flag} {countryName} 13 | </span> 14 | ); 15 | } 16 | export default CountryFlagAndName; 17 | -------------------------------------------------------------------------------- /02-home-away-project/components/card/FavoriteToggleButton.tsx: -------------------------------------------------------------------------------- 1 | import { FaHeart } from 'react-icons/fa'; 2 | import { Button } from '../ui/button'; 3 | import { auth } from '@clerk/nextjs/server'; 4 | import { CardSignInButton } from '../form/Buttons'; 5 | import { fetchFavoriteId } from '@/utils/actions'; 6 | import FavoriteToggleForm from './FavoriteToggleForm'; 7 | async function FavoriteToggleButton({ propertyId }: { propertyId: string }) { 8 | const { userId } = auth(); 9 | if (!userId) return <CardSignInButton />; 10 | const favoriteId = await fetchFavoriteId({ propertyId }); 11 | return <FavoriteToggleForm favoriteId={favoriteId} propertyId={propertyId} />; 12 | } 13 | export default FavoriteToggleButton; 14 | -------------------------------------------------------------------------------- /02-home-away-project/components/card/FavoriteToggleForm.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { usePathname } from 'next/navigation'; 4 | import FormContainer from '../form/FormContainer'; 5 | import { toggleFavoriteAction } from '@/utils/actions'; 6 | import { CardSubmitButton } from '../form/Buttons'; 7 | 8 | type FavoriteToggleFormProps = { 9 | propertyId: string; 10 | favoriteId: string | null; 11 | }; 12 | 13 | function FavoriteToggleForm({ 14 | propertyId, 15 | favoriteId, 16 | }: FavoriteToggleFormProps) { 17 | const pathname = usePathname(); 18 | const toggleAction = toggleFavoriteAction.bind(null, { 19 | propertyId, 20 | favoriteId, 21 | pathname, 22 | }); 23 | return ( 24 | <FormContainer action={toggleAction}> 25 | <CardSubmitButton isFavorite={favoriteId ? true : false} /> 26 | </FormContainer> 27 | ); 28 | } 29 | export default FavoriteToggleForm; 30 | -------------------------------------------------------------------------------- /02-home-away-project/components/card/LoadingCards.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from '../ui/skeleton'; 2 | 3 | function LoadingCards() { 4 | return ( 5 | <div className='mt-4 gap-8 grid sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4'> 6 | <SkeletonCard /> 7 | <SkeletonCard /> 8 | <SkeletonCard /> 9 | <SkeletonCard /> 10 | </div> 11 | ); 12 | } 13 | 14 | export function SkeletonCard() { 15 | return ( 16 | <div> 17 | <Skeleton className='h-[300px] rounded-md' /> 18 | <Skeleton className='h-4 mt-2 w-3/4' /> 19 | <Skeleton className='h-4 mt-2 w-1/2' /> 20 | </div> 21 | ); 22 | } 23 | 24 | export default LoadingCards; 25 | -------------------------------------------------------------------------------- /02-home-away-project/components/card/PropertyCard.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import Link from 'next/link'; 3 | import CountryFlagAndName from './CountryFlagAndName'; 4 | import PropertyRating from './PropertyRating'; 5 | import FavoriteToggleButton from './FavoriteToggleButton'; 6 | import { PropertyCardProps } from '@/utils/types'; 7 | import { formatCurrency } from '@/utils/format'; 8 | 9 | function PropertyCard({ property }: { property: PropertyCardProps }) { 10 | const { name, image, price } = property; 11 | const { country, id: propertyId, tagline } = property; 12 | 13 | return ( 14 | <article className='group relative'> 15 | <Link href={`/properties/${propertyId}`}> 16 | <div className='relative h-[300px] mb-2 overflow-hidden rounded-md'> 17 | <Image 18 | src={image} 19 | fill 20 | sizes='(max-width:768px) 100vw, 50vw' 21 | alt={name} 22 | className='rounded-md object-cover transform group-hover:scale-110 transition-transform duration-500' 23 | /> 24 | </div> 25 | <div className='flex justify-between items-center'> 26 | <h3 className='text-sm font-semibold mt-1'> 27 | {name.substring(0, 30)} 28 | </h3> 29 | {/* property rating */} 30 | <PropertyRating inPage={false} propertyId={propertyId} /> 31 | </div> 32 | <p className='text-sm mt-1 text-muted-foreground'> 33 | {tagline.substring(0, 40)} 34 | </p> 35 | <div className='flex justify-between items-center mt-1'> 36 | <p className='text-sm mt-1'> 37 | <span className='font-semibold'>{formatCurrency(price)} </span> 38 | night 39 | </p> 40 | {/* country and flag */} 41 | <CountryFlagAndName countryCode={country} /> 42 | </div> 43 | </Link> 44 | <div className='absolute top-5 right-5 z-5'> 45 | {/* favorite toggle button */} 46 | <FavoriteToggleButton propertyId={propertyId} /> 47 | </div> 48 | </article> 49 | ); 50 | } 51 | export default PropertyCard; 52 | -------------------------------------------------------------------------------- /02-home-away-project/components/card/PropertyRating.tsx: -------------------------------------------------------------------------------- 1 | import { fetchPropertyRating } from '@/utils/actions'; 2 | import { FaStar } from 'react-icons/fa'; 3 | 4 | async function PropertyRating({ 5 | propertyId, 6 | inPage, 7 | }: { 8 | propertyId: string; 9 | inPage: boolean; 10 | }) { 11 | const { rating, count } = await fetchPropertyRating(propertyId); 12 | if (count === 0) return null; 13 | const className = `flex gap-1 items-center ${inPage ? 'text-md' : 'text-xs'}`; 14 | const countText = count === 1 ? 'review' : 'reviews'; 15 | const countValue = `(${count}) ${inPage ? countText : ''}`; 16 | return ( 17 | <span className={className}> 18 | <FaStar className='w-3 h-3' /> 19 | {rating} {countValue} 20 | </span> 21 | ); 22 | } 23 | 24 | export default PropertyRating; 25 | -------------------------------------------------------------------------------- /02-home-away-project/components/form/AmenitiesInput.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { useState } from 'react'; 3 | import { amenities, Amenity } from '@/utils/amenities'; 4 | import { Checkbox } from '@/components/ui/checkbox'; 5 | 6 | function AmenitiesInput({ defaultValue }: { defaultValue?: Amenity[] }) { 7 | const amenitiesWithIcons = defaultValue?.map(({ name, selected }) => ({ 8 | name, 9 | selected, 10 | icon: amenities.find((amenity) => amenity.name === name)!.icon, 11 | })); 12 | const [selectedAmenities, setSelectedAmenities] = useState<Amenity[]>( 13 | amenitiesWithIcons || amenities 14 | ); 15 | const handleChange = (amenity: Amenity) => { 16 | setSelectedAmenities((prev) => { 17 | return prev.map((a) => { 18 | if (a.name === amenity.name) { 19 | return { ...a, selected: !a.selected }; 20 | } 21 | return a; 22 | }); 23 | }); 24 | }; 25 | 26 | return ( 27 | <section> 28 | <input 29 | type='hidden' 30 | name='amenities' 31 | value={JSON.stringify(selectedAmenities)} 32 | /> 33 | <div className='grid grid-cols-2 gap-4'> 34 | {selectedAmenities.map((amenity) => { 35 | return ( 36 | <div key={amenity.name} className='flex items-center space-x-2'> 37 | <Checkbox 38 | id={amenity.name} 39 | checked={amenity.selected} 40 | onCheckedChange={() => handleChange(amenity)} 41 | /> 42 | <label 43 | htmlFor={amenity.name} 44 | className='text-sm font-medium leading-none capitalize flex gap-x-2 items-center' 45 | > 46 | {amenity.name} <amenity.icon className='w-4 h-4' /> 47 | </label> 48 | </div> 49 | ); 50 | })} 51 | </div> 52 | </section> 53 | ); 54 | } 55 | export default AmenitiesInput; 56 | -------------------------------------------------------------------------------- /02-home-away-project/components/form/Buttons.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ReloadIcon } from '@radix-ui/react-icons'; 4 | import { useFormStatus } from 'react-dom'; 5 | import { Button } from '@/components/ui/button'; 6 | import { SignInButton } from '@clerk/nextjs'; 7 | import { FaRegHeart, FaHeart } from 'react-icons/fa'; 8 | import { LuTrash2, LuPenSquare } from 'react-icons/lu'; 9 | type btnSize = 'default' | 'lg' | 'sm'; 10 | 11 | type SubmitButtonProps = { 12 | className?: string; 13 | text?: string; 14 | size?: btnSize; 15 | }; 16 | 17 | export function SubmitButton({ 18 | className = '', 19 | text = 'submit', 20 | size = 'lg', 21 | }: SubmitButtonProps) { 22 | const { pending } = useFormStatus(); 23 | 24 | return ( 25 | <Button 26 | type='submit' 27 | disabled={pending} 28 | className={`capitalize ${className} `} 29 | size={size} 30 | > 31 | {pending ? ( 32 | <> 33 | <ReloadIcon className='mr-2 h-4 w-4 animate-spin' /> 34 | Please wait... 35 | </> 36 | ) : ( 37 | text 38 | )} 39 | </Button> 40 | ); 41 | } 42 | 43 | export const CardSignInButton = () => { 44 | return ( 45 | <SignInButton mode='modal'> 46 | <Button 47 | type='button' 48 | size='icon' 49 | variant='outline' 50 | className='p-2 cursor-pointer' 51 | asChild 52 | > 53 | <FaRegHeart /> 54 | </Button> 55 | </SignInButton> 56 | ); 57 | }; 58 | 59 | export const CardSubmitButton = ({ isFavorite }: { isFavorite: boolean }) => { 60 | const { pending } = useFormStatus(); 61 | return ( 62 | <Button 63 | type='submit' 64 | size='icon' 65 | variant='outline' 66 | className='p-2 cursor-pointer' 67 | > 68 | {pending ? ( 69 | <ReloadIcon className='animate-spin' /> 70 | ) : isFavorite ? ( 71 | <FaHeart /> 72 | ) : ( 73 | <FaRegHeart /> 74 | )} 75 | </Button> 76 | ); 77 | }; 78 | 79 | type actionType = 'edit' | 'delete'; 80 | export const IconButton = ({ actionType }: { actionType: actionType }) => { 81 | const { pending } = useFormStatus(); 82 | 83 | const renderIcon = () => { 84 | switch (actionType) { 85 | case 'edit': 86 | return <LuPenSquare />; 87 | case 'delete': 88 | return <LuTrash2 />; 89 | default: 90 | const never: never = actionType; 91 | throw new Error(`Invalid action type: ${never}`); 92 | } 93 | }; 94 | 95 | return ( 96 | <Button 97 | type='submit' 98 | size='icon' 99 | variant='link' 100 | className='p-2 cursor-pointer' 101 | > 102 | {pending ? <ReloadIcon className=' animate-spin' /> : renderIcon()} 103 | </Button> 104 | ); 105 | }; 106 | -------------------------------------------------------------------------------- /02-home-away-project/components/form/CategoriesInput.tsx: -------------------------------------------------------------------------------- 1 | import { Label } from '@/components/ui/label'; 2 | import { categories } from '@/utils/categories'; 3 | import { 4 | Select, 5 | SelectContent, 6 | SelectItem, 7 | SelectTrigger, 8 | SelectValue, 9 | } from '@/components/ui/select'; 10 | 11 | const name = 'category'; 12 | 13 | function CategoriesInput({ defaultValue }: { defaultValue?: string }) { 14 | return ( 15 | <div className='mb-2'> 16 | <Label htmlFor={name} className='capitalize'> 17 | Categories 18 | </Label> 19 | <Select 20 | defaultValue={defaultValue || categories[0].label} 21 | name={name} 22 | required 23 | > 24 | <SelectTrigger id={name}> 25 | <SelectValue /> 26 | </SelectTrigger> 27 | <SelectContent> 28 | {categories.map((item) => { 29 | return ( 30 | <SelectItem key={item.label} value={item.label}> 31 | <span className='flex items-center gap-2'> 32 | <item.icon /> {item.label} 33 | </span> 34 | </SelectItem> 35 | ); 36 | })} 37 | </SelectContent> 38 | </Select> 39 | </div> 40 | ); 41 | } 42 | export default CategoriesInput; 43 | -------------------------------------------------------------------------------- /02-home-away-project/components/form/CounterInput.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { Card, CardHeader } from '@/components/ui/card'; 3 | import { LuMinus, LuPlus } from 'react-icons/lu'; 4 | 5 | import { Button } from '../ui/button'; 6 | import { useState } from 'react'; 7 | 8 | function CounterInput({ 9 | detail, 10 | defaultValue, 11 | }: { 12 | detail: string; 13 | defaultValue?: number; 14 | }) { 15 | const [count, setCount] = useState(defaultValue || 0); 16 | 17 | const increaseCount = () => { 18 | setCount((prevCount) => prevCount + 1); 19 | }; 20 | 21 | const decreaseCount = () => { 22 | setCount((prevCount) => { 23 | if (prevCount > 0) { 24 | return prevCount - 1; 25 | } 26 | return prevCount; 27 | }); 28 | }; 29 | 30 | return ( 31 | <Card className='mb-4'> 32 | {/* input */} 33 | <input type='hidden' name={detail} value={count} /> 34 | <CardHeader className='flex flex-col gap-y-5'> 35 | <div className='flex items-center justify-between flex-wrap'> 36 | <div className='flex flex-col'> 37 | <h2 className='font-medium capitalize'>{detail}</h2> 38 | <p className='text-muted-foreground text-sm'> 39 | Specify the number of {detail} 40 | </p> 41 | </div> 42 | <div className='flex items-center gap-4'> 43 | <Button 44 | variant='outline' 45 | size='icon' 46 | type='button' 47 | onClick={decreaseCount} 48 | > 49 | <LuMinus className='w-5 h-5 text-primary' /> 50 | </Button> 51 | <span className='text-xl font-bold w-5 text-center'>{count}</span> 52 | <Button 53 | variant='outline' 54 | size='icon' 55 | type='button' 56 | onClick={increaseCount} 57 | > 58 | <LuPlus className='w-5 h-5 text-primary' /> 59 | </Button> 60 | </div> 61 | </div> 62 | </CardHeader> 63 | </Card> 64 | ); 65 | } 66 | export default CounterInput; 67 | -------------------------------------------------------------------------------- /02-home-away-project/components/form/CountriesInput.tsx: -------------------------------------------------------------------------------- 1 | import { Label } from '@/components/ui/label'; 2 | import { formattedCountries } from '@/utils/countries'; 3 | import { 4 | Select, 5 | SelectContent, 6 | SelectItem, 7 | SelectTrigger, 8 | SelectValue, 9 | } from '@/components/ui/select'; 10 | 11 | const name = 'country'; 12 | 13 | function CountriesInput({ defaultValue }: { defaultValue?: string }) { 14 | return ( 15 | <div className='mb-2'> 16 | <Label htmlFor={name} className='capitalize'> 17 | country 18 | </Label> 19 | 20 | <Select 21 | defaultValue={defaultValue || formattedCountries[0].code} 22 | name={name} 23 | required 24 | > 25 | <SelectTrigger id={name}> 26 | <SelectValue /> 27 | </SelectTrigger> 28 | <SelectContent> 29 | {formattedCountries.map((item) => { 30 | return ( 31 | <SelectItem key={item.code} value={item.code}> 32 | <span className='flex items-center gap-2'> 33 | {item.flag} {item.name} 34 | </span> 35 | </SelectItem> 36 | ); 37 | })} 38 | </SelectContent> 39 | </Select> 40 | </div> 41 | ); 42 | } 43 | export default CountriesInput; 44 | -------------------------------------------------------------------------------- /02-home-away-project/components/form/FormContainer.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useFormState } from 'react-dom'; 4 | import { useEffect } from 'react'; 5 | import { useToast } from '@/components/ui/use-toast'; 6 | import { actionFunction } from '@/utils/types'; 7 | 8 | const initialState = { 9 | message: '', 10 | }; 11 | 12 | function FormContainer({ 13 | action, 14 | children, 15 | }: { 16 | action: actionFunction; 17 | children: React.ReactNode; 18 | }) { 19 | const [state, formAction] = useFormState(action, initialState); 20 | const { toast } = useToast(); 21 | 22 | useEffect(() => { 23 | if (state.message) { 24 | toast({ description: state.message }); 25 | } 26 | }, [state]); 27 | 28 | return <form action={formAction}>{children}</form>; 29 | } 30 | 31 | export default FormContainer; 32 | -------------------------------------------------------------------------------- /02-home-away-project/components/form/FormInput.tsx: -------------------------------------------------------------------------------- 1 | import { Label } from '@/components/ui/label'; 2 | import { Input } from '@/components/ui/input'; 3 | 4 | type FormInputProps = { 5 | name: string; 6 | type: string; 7 | label?: string; 8 | defaultValue?: string; 9 | placeholder?: string; 10 | }; 11 | 12 | function FormInput(props: FormInputProps) { 13 | const { label, name, type, defaultValue, placeholder } = props; 14 | return ( 15 | <div className='mb-2'> 16 | <Label htmlFor={name} className='capitalize'> 17 | {label || name} 18 | </Label> 19 | <Input 20 | id={name} 21 | name={name} 22 | type={type} 23 | defaultValue={defaultValue} 24 | placeholder={placeholder} 25 | required 26 | /> 27 | </div> 28 | ); 29 | } 30 | export default FormInput; 31 | -------------------------------------------------------------------------------- /02-home-away-project/components/form/ImageInput.tsx: -------------------------------------------------------------------------------- 1 | import { Input } from '../ui/input'; 2 | import { Label } from '../ui/label'; 3 | 4 | function ImageInput() { 5 | const name = 'image'; 6 | return ( 7 | <div className='mb-2'> 8 | <Label htmlFor={name} className='capitalize'> 9 | Image 10 | </Label> 11 | <Input 12 | id={name} 13 | name={name} 14 | type='file' 15 | required 16 | accept='image/*' 17 | className='max-w-xs' 18 | /> 19 | </div> 20 | ); 21 | } 22 | export default ImageInput; 23 | -------------------------------------------------------------------------------- /02-home-away-project/components/form/ImageInputContainer.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { useState } from 'react'; 3 | import Image from 'next/image'; 4 | import { Button } from '../ui/button'; 5 | import FormContainer from './FormContainer'; 6 | import ImageInput from './ImageInput'; 7 | import { SubmitButton } from './Buttons'; 8 | import { type actionFunction } from '@/utils/types'; 9 | import { LuUser2 } from 'react-icons/lu'; 10 | 11 | type ImageInputContainerProps = { 12 | image: string; 13 | name: string; 14 | action: actionFunction; 15 | text: string; 16 | children?: React.ReactNode; 17 | }; 18 | 19 | function ImageInputContainer(props: ImageInputContainerProps) { 20 | const { image, name, action, text } = props; 21 | const [isUpdateFormVisible, setUpdateFormVisible] = useState(false); 22 | 23 | const userIcon = ( 24 | <LuUser2 className='w-24 h-24 bg-primary rounded text-white mb-4' /> 25 | ); 26 | return ( 27 | <div> 28 | {image ? ( 29 | <Image 30 | src={image} 31 | alt={name} 32 | width={100} 33 | height={100} 34 | className='rounded object-cover mb-4 w-24 h-24' 35 | /> 36 | ) : ( 37 | userIcon 38 | )} 39 | <Button 40 | variant='outline' 41 | size='sm' 42 | onClick={() => setUpdateFormVisible((prev) => !prev)} 43 | > 44 | {text} 45 | </Button> 46 | {isUpdateFormVisible && ( 47 | <div className='max-w-lg mt-4'> 48 | <FormContainer action={action}> 49 | {props.children} 50 | <ImageInput /> 51 | <SubmitButton size='sm' /> 52 | </FormContainer> 53 | </div> 54 | )} 55 | </div> 56 | ); 57 | } 58 | export default ImageInputContainer; 59 | -------------------------------------------------------------------------------- /02-home-away-project/components/form/PriceInput.tsx: -------------------------------------------------------------------------------- 1 | import { Label } from '@/components/ui/label'; 2 | import { Input } from '../ui/input'; 3 | import { Prisma } from '@prisma/client'; 4 | 5 | // const name = Prisma.PropertyScalarFieldEnum.price 6 | 7 | type PriceInputProps = { 8 | defaultValue?: number; 9 | }; 10 | 11 | function PriceInput({ defaultValue }: PriceInputProps) { 12 | const name = 'price'; 13 | return ( 14 | <div className='mb-2'> 15 | <Label htmlFor={name} className='capitalize'> 16 | Price ($) 17 | </Label> 18 | <Input 19 | id={name} 20 | type='number' 21 | name={name} 22 | min={0} 23 | defaultValue={defaultValue || 100} 24 | required 25 | /> 26 | </div> 27 | ); 28 | } 29 | export default PriceInput; 30 | -------------------------------------------------------------------------------- /02-home-away-project/components/form/RatingInput.tsx: -------------------------------------------------------------------------------- 1 | import { Label } from '@/components/ui/label'; 2 | import { 3 | Select, 4 | SelectContent, 5 | SelectItem, 6 | SelectTrigger, 7 | SelectValue, 8 | } from '@/components/ui/select'; 9 | 10 | const RatingInput = ({ 11 | name, 12 | labelText, 13 | }: { 14 | name: string; 15 | labelText?: string; 16 | }) => { 17 | const numbers = Array.from({ length: 5 }, (_, i) => { 18 | const value = i + 1; 19 | return value.toString(); 20 | }).reverse(); 21 | 22 | return ( 23 | <div className='mb-2 max-w-xs'> 24 | <Label htmlFor={name} className='capitalize'> 25 | {labelText || name} 26 | </Label> 27 | <Select defaultValue={numbers[0]} name={name} required> 28 | <SelectTrigger> 29 | <SelectValue /> 30 | </SelectTrigger> 31 | <SelectContent> 32 | {numbers.map((number) => { 33 | return ( 34 | <SelectItem key={number} value={number}> 35 | {number} 36 | </SelectItem> 37 | ); 38 | })} 39 | </SelectContent> 40 | </Select> 41 | </div> 42 | ); 43 | }; 44 | 45 | export default RatingInput; 46 | -------------------------------------------------------------------------------- /02-home-away-project/components/form/TextAreaInput.tsx: -------------------------------------------------------------------------------- 1 | import { Label } from '@/components/ui/label'; 2 | import { Textarea } from '@/components/ui/textarea'; 3 | 4 | type TextAreaInputProps = { 5 | name: string; 6 | labelText?: string; 7 | defaultValue?: string; 8 | }; 9 | 10 | function TextAreaInput({ name, labelText, defaultValue }: TextAreaInputProps) { 11 | return ( 12 | <div className='mb-2'> 13 | <Label htmlFor={name} className='capitalize'> 14 | {labelText || name} 15 | </Label> 16 | <Textarea 17 | id={name} 18 | name={name} 19 | defaultValue={defaultValue || tempDefaultDescription} 20 | rows={5} 21 | required 22 | className='leading-loose' 23 | /> 24 | </div> 25 | ); 26 | } 27 | 28 | const tempDefaultDescription = 29 | 'Glamping Tuscan Style in an Aframe Cabin Tent, nestled in a beautiful olive orchard. AC, heat, Queen Bed, TV, Wi-Fi and an amazing view. Close to Weeki Wachee River State Park, mermaids, manatees, Chassahwitzka River and on the SC Bike Path. Kayaks available for rivers. Bathhouse, fire pit, Kitchenette, fresh eggs. Relax & enjoy fresh country air. No pets please. Ducks, hens and roosters roam the grounds. We have a Pot Cake Rescue from Bimini, Retriever and Pom dog. The space is inspiring and relaxing. Enjoy the beauty of the orchard. Spring trees are in blossom and harvested in Fall. We have a farm store where we sell our farm to table products'; 30 | 31 | export default TextAreaInput; 32 | -------------------------------------------------------------------------------- /02-home-away-project/components/home/CategoriesList.tsx: -------------------------------------------------------------------------------- 1 | import { categories } from '@/utils/categories'; 2 | import { ScrollArea, ScrollBar } from '../ui/scroll-area'; 3 | import Link from 'next/link'; 4 | 5 | function CategoriesList({ 6 | category, 7 | search, 8 | }: { 9 | category?: string; 10 | search?: string; 11 | }) { 12 | const searchTerm = search ? `&search=${search}` : ''; 13 | return ( 14 | <section> 15 | <ScrollArea className='py-6'> 16 | <div className='flex gap-x-4'> 17 | {categories.map((item) => { 18 | const isActive = item.label === category; 19 | return ( 20 | <Link 21 | key={item.label} 22 | href={`/?category=${item.label}${searchTerm}`} 23 | > 24 | <article 25 | className={`p-3 flex flex-col items-center cursor-pointer duration-300 hover:text-primary w-[100px] ${ 26 | isActive ? 'text-primary' : '' 27 | }`} 28 | > 29 | <item.icon className='w-8 h-8' /> 30 | <p className='capitalize text-sm mt-1'>{item.label}</p> 31 | </article> 32 | </Link> 33 | ); 34 | })} 35 | </div> 36 | <ScrollBar orientation='horizontal' /> 37 | </ScrollArea> 38 | </section> 39 | ); 40 | } 41 | export default CategoriesList; 42 | -------------------------------------------------------------------------------- /02-home-away-project/components/home/EmptyList.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import { Button } from '../ui/button'; 3 | function EmptyList({ 4 | heading = 'No items in the list.', 5 | message = 'Keep exploring our properties', 6 | btnText = 'back home', 7 | }: { 8 | heading?: string; 9 | message?: string; 10 | btnText?: string; 11 | }) { 12 | return ( 13 | <div className='mt-4'> 14 | <h2 className='text-xl font-bold'>{heading}</h2> 15 | <p className='text-lg'>{message}</p> 16 | <Button asChild className='mt-4 capitalize' size='lg'> 17 | <Link href='/'>{btnText}</Link> 18 | </Button> 19 | </div> 20 | ); 21 | } 22 | export default EmptyList; 23 | -------------------------------------------------------------------------------- /02-home-away-project/components/home/PropertiesContainer.tsx: -------------------------------------------------------------------------------- 1 | import { fetchProperties } from '@/utils/actions'; 2 | import PropertiesList from './PropertiesList'; 3 | import EmptyList from './EmptyList'; 4 | import type { PropertyCardProps } from '@/utils/types'; 5 | 6 | async function PropertiesContainer({ 7 | category, 8 | search, 9 | }: { 10 | category?: string; 11 | search?: string; 12 | }) { 13 | const properties: PropertyCardProps[] = await fetchProperties({ 14 | category, 15 | search, 16 | }); 17 | if (properties.length === 0) { 18 | return ( 19 | <EmptyList 20 | heading='No results.' 21 | message='Try changing or removing some of your filters.' 22 | btnText='Clear Filters' 23 | /> 24 | ); 25 | } 26 | 27 | return <PropertiesList properties={properties} />; 28 | } 29 | export default PropertiesContainer; 30 | -------------------------------------------------------------------------------- /02-home-away-project/components/home/PropertiesList.tsx: -------------------------------------------------------------------------------- 1 | import PropertyCard from '../card/PropertyCard'; 2 | import type { PropertyCardProps } from '@/utils/types'; 3 | 4 | function PropertiesList({ properties }: { properties: PropertyCardProps[] }) { 5 | return ( 6 | <section className='mt-4 gap-8 grid sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4'> 7 | {properties.map((property) => { 8 | return <PropertyCard key={property.id} property={property} />; 9 | })} 10 | </section> 11 | ); 12 | } 13 | export default PropertiesList; 14 | -------------------------------------------------------------------------------- /02-home-away-project/components/navbar/DarkMode.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import { MoonIcon, SunIcon } from '@radix-ui/react-icons'; 5 | import { useTheme } from 'next-themes'; 6 | 7 | import { Button } from '@/components/ui/button'; 8 | import { 9 | DropdownMenu, 10 | DropdownMenuContent, 11 | DropdownMenuItem, 12 | DropdownMenuTrigger, 13 | } from '@/components/ui/dropdown-menu'; 14 | 15 | export default function ModeToggle() { 16 | const { setTheme } = useTheme(); 17 | 18 | return ( 19 | <DropdownMenu> 20 | <DropdownMenuTrigger asChild> 21 | <Button variant='outline' size='icon'> 22 | <SunIcon className='h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0' /> 23 | <MoonIcon className='absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100' /> 24 | <span className='sr-only'>Toggle theme</span> 25 | </Button> 26 | </DropdownMenuTrigger> 27 | <DropdownMenuContent align='end'> 28 | <DropdownMenuItem onClick={() => setTheme('light')}> 29 | Light 30 | </DropdownMenuItem> 31 | <DropdownMenuItem onClick={() => setTheme('dark')}> 32 | Dark 33 | </DropdownMenuItem> 34 | <DropdownMenuItem onClick={() => setTheme('system')}> 35 | System 36 | </DropdownMenuItem> 37 | </DropdownMenuContent> 38 | </DropdownMenu> 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /02-home-away-project/components/navbar/LinksDropdown.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | DropdownMenu, 3 | DropdownMenuContent, 4 | DropdownMenuItem, 5 | DropdownMenuTrigger, 6 | DropdownMenuSeparator, 7 | } from '@/components/ui/dropdown-menu'; 8 | import { LuAlignLeft } from 'react-icons/lu'; 9 | import Link from 'next/link'; 10 | import { Button } from '../ui/button'; 11 | import UserIcon from './UserIcon'; 12 | import { links } from '@/utils/links'; 13 | import SignOutLink from './SignOutLink'; 14 | import { SignedOut, SignedIn, SignInButton, SignUpButton } from '@clerk/nextjs'; 15 | import { auth } from '@clerk/nextjs/server'; 16 | function LinksDropdown() { 17 | const { userId } = auth(); 18 | const isAdminUser = userId === process.env.ADMIN_USER_ID; 19 | return ( 20 | <DropdownMenu> 21 | <DropdownMenuTrigger asChild> 22 | <Button variant='outline' className='flex gap-4 max-w-[100px]'> 23 | <LuAlignLeft className='w-6 h-6' /> 24 | <UserIcon /> 25 | </Button> 26 | </DropdownMenuTrigger> 27 | <DropdownMenuContent className='w-52' align='start' sideOffset={10}> 28 | <SignedOut> 29 | <DropdownMenuItem> 30 | <SignInButton mode='modal'> 31 | <button className='w-full text-left'>Login</button> 32 | </SignInButton> 33 | </DropdownMenuItem> 34 | <DropdownMenuSeparator /> 35 | <DropdownMenuItem> 36 | <SignUpButton mode='modal'> 37 | <button className='w-full text-left'>Register</button> 38 | </SignUpButton> 39 | </DropdownMenuItem> 40 | </SignedOut> 41 | <SignedIn> 42 | {links.map((link) => { 43 | if (link.label === 'admin' && !isAdminUser) return null; 44 | return ( 45 | <DropdownMenuItem key={link.href}> 46 | <Link href={link.href} className='capitalize w-full'> 47 | {link.label} 48 | </Link> 49 | </DropdownMenuItem> 50 | ); 51 | })} 52 | <DropdownMenuSeparator /> 53 | <DropdownMenuItem> 54 | <SignOutLink /> 55 | </DropdownMenuItem> 56 | </SignedIn> 57 | </DropdownMenuContent> 58 | </DropdownMenu> 59 | ); 60 | } 61 | export default LinksDropdown; 62 | -------------------------------------------------------------------------------- /02-home-away-project/components/navbar/Logo.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import { LuTent } from 'react-icons/lu'; 3 | import { Button } from '../ui/button'; 4 | 5 | function Logo() { 6 | return ( 7 | <Button size='icon' asChild> 8 | <Link href='/'> 9 | <LuTent className='w-6 h-6' /> 10 | </Link> 11 | </Button> 12 | ); 13 | } 14 | export default Logo; 15 | -------------------------------------------------------------------------------- /02-home-away-project/components/navbar/NavSearch.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { Input } from '../ui/input'; 3 | import { useSearchParams, useRouter } from 'next/navigation'; 4 | import { useDebouncedCallback } from 'use-debounce'; 5 | import { useState, useEffect } from 'react'; 6 | 7 | function NavSearch() { 8 | const searchParams = useSearchParams(); 9 | const { replace } = useRouter(); 10 | 11 | const [search, setSearch] = useState( 12 | searchParams.get('search')?.toString() || '' 13 | ); 14 | const handleSearch = useDebouncedCallback((value: string) => { 15 | const params = new URLSearchParams(searchParams); 16 | if (value) { 17 | params.set('search', value); 18 | } else { 19 | params.delete('search'); 20 | } 21 | replace(`/?${params.toString()}`); 22 | }, 500); 23 | 24 | useEffect(() => { 25 | if (!searchParams.get('search')) { 26 | setSearch(''); 27 | } 28 | }, [searchParams.get('search')]); 29 | 30 | return ( 31 | <Input 32 | type='text' 33 | placeholder='find a property...' 34 | className='max-w-xs dark:bg-muted' 35 | onChange={(e) => { 36 | setSearch(e.target.value); 37 | handleSearch(e.target.value); 38 | }} 39 | value={search} 40 | /> 41 | ); 42 | } 43 | export default NavSearch; 44 | -------------------------------------------------------------------------------- /02-home-away-project/components/navbar/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import NavSearch from './NavSearch'; 2 | import LinksDropdown from './LinksDropdown'; 3 | import DarkMode from './DarkMode'; 4 | import Logo from './Logo'; 5 | 6 | function Navbar() { 7 | return ( 8 | <nav className='border-b'> 9 | <div className='container flex flex-col sm:flex-row sm:justify-between sm:items-center flex-wrap gap-4 py-8'> 10 | <Logo /> 11 | <NavSearch /> 12 | <div className='flex gap-4 items-center'> 13 | <DarkMode /> 14 | <LinksDropdown /> 15 | </div> 16 | </div> 17 | </nav> 18 | ); 19 | } 20 | export default Navbar; 21 | -------------------------------------------------------------------------------- /02-home-away-project/components/navbar/SignOutLink.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { SignOutButton } from '@clerk/nextjs'; 4 | import { useToast } from '../ui/use-toast'; 5 | 6 | function SignOutLink() { 7 | const { toast } = useToast(); 8 | const handleLogout = () => { 9 | toast({ description: 'You have been signed out.' }); 10 | }; 11 | 12 | return ( 13 | <SignOutButton redirectUrl='/'> 14 | <button className='w-full text-left' onClick={handleLogout}> 15 | Logout 16 | </button> 17 | </SignOutButton> 18 | ); 19 | } 20 | export default SignOutLink; 21 | -------------------------------------------------------------------------------- /02-home-away-project/components/navbar/UserIcon.tsx: -------------------------------------------------------------------------------- 1 | import { LuUser2 } from 'react-icons/lu'; 2 | import { fetchProfileImage } from '@/utils/actions'; 3 | async function UserIcon() { 4 | const profileImage = await fetchProfileImage(); 5 | if (profileImage) { 6 | return ( 7 | <img src={profileImage} className='w-6 h-6 rounded-full object-cover' /> 8 | ); 9 | } 10 | return <LuUser2 className='w-6 h-6 bg-primary rounded-full text-white' />; 11 | } 12 | export default UserIcon; 13 | -------------------------------------------------------------------------------- /02-home-away-project/components/properties/Amenities.tsx: -------------------------------------------------------------------------------- 1 | import { Amenity } from '@/utils/amenities'; 2 | import { LuFolderCheck } from 'react-icons/lu'; 3 | import Title from './Title'; 4 | 5 | function Amenities({ amenities }: { amenities: string }) { 6 | const amenitiesList: Amenity[] = JSON.parse(amenities as string); 7 | const noAmenities = amenitiesList.every((amenity) => !amenity.selected); 8 | if (noAmenities) return null; 9 | 10 | return ( 11 | <div className='mt-4'> 12 | <Title text='What this place offers' /> 13 | <div className='grid md:grid-cols-2 gap-x-4'> 14 | {amenitiesList.map((amenity) => { 15 | if (!amenity.selected) return null; 16 | return ( 17 | <div key={amenity.name} className='flex items-center gap-x-4 mb-2'> 18 | <LuFolderCheck className='h-6 w-6 text-primary' /> 19 | <span className='font-light text-sm capitalize'> 20 | {amenity.name} 21 | </span> 22 | </div> 23 | ); 24 | })} 25 | </div> 26 | </div> 27 | ); 28 | } 29 | export default Amenities; 30 | -------------------------------------------------------------------------------- /02-home-away-project/components/properties/BreadCrumbs.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Breadcrumb, 3 | BreadcrumbItem, 4 | BreadcrumbLink, 5 | BreadcrumbList, 6 | BreadcrumbPage, 7 | BreadcrumbSeparator, 8 | } from '@/components/ui/breadcrumb'; 9 | 10 | function BreadCrumbs({ name }: { name: string }) { 11 | return ( 12 | <Breadcrumb> 13 | <BreadcrumbList> 14 | <BreadcrumbItem> 15 | <BreadcrumbLink href='/'>Home</BreadcrumbLink> 16 | </BreadcrumbItem> 17 | <BreadcrumbSeparator /> 18 | <BreadcrumbItem> 19 | <BreadcrumbPage>{name}</BreadcrumbPage> 20 | </BreadcrumbItem> 21 | </BreadcrumbList> 22 | </Breadcrumb> 23 | ); 24 | } 25 | export default BreadCrumbs; 26 | -------------------------------------------------------------------------------- /02-home-away-project/components/properties/Description.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { useState } from 'react'; 3 | import { Button } from '@/components/ui/button'; 4 | import Title from './Title'; 5 | 6 | function Description({ description }: { description: string }) { 7 | const [isFullDescriptionShown, setIsFullDescriptionShown] = useState(false); 8 | 9 | const words = description.split(' '); 10 | const isLongDescription = words.length > 100; 11 | 12 | const toggleDescription = () => { 13 | setIsFullDescriptionShown(!isFullDescriptionShown); 14 | }; 15 | 16 | const displayedDescription = 17 | isLongDescription && !isFullDescriptionShown 18 | ? words.splice(0, 100).join(' ') + '...' 19 | : description; 20 | 21 | return ( 22 | <article className='mt-4'> 23 | <Title text='Description' /> 24 | <p className='text-muted-foreground font-light leading-loose'> 25 | {displayedDescription} 26 | </p> 27 | {isLongDescription && ( 28 | <Button variant='link' className='pl-0' onClick={toggleDescription}> 29 | {isFullDescriptionShown ? 'Show less' : 'Show more'} 30 | </Button> 31 | )} 32 | </article> 33 | ); 34 | } 35 | export default Description; 36 | -------------------------------------------------------------------------------- /02-home-away-project/components/properties/ImageContainer.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | 3 | function ImageContainer({ 4 | mainImage, 5 | name, 6 | }: { 7 | mainImage: string; 8 | name: string; 9 | }) { 10 | return ( 11 | <section className='h-[300px] md:h-[500px] relative mt-8'> 12 | <Image 13 | src={mainImage} 14 | fill 15 | sizes='100vw' 16 | alt={name} 17 | className='object-cover rounded' 18 | priority 19 | /> 20 | </section> 21 | ); 22 | } 23 | export default ImageContainer; 24 | -------------------------------------------------------------------------------- /02-home-away-project/components/properties/PropertyDetails.tsx: -------------------------------------------------------------------------------- 1 | import { formatQuantity } from '@/utils/format'; 2 | 3 | type PropertyDetailsProps = { 4 | details: { 5 | bedrooms: number; 6 | baths: number; 7 | guests: number; 8 | beds: number; 9 | }; 10 | }; 11 | 12 | function PropertyDetails({ 13 | details: { bedrooms, baths, guests, beds }, 14 | }: PropertyDetailsProps) { 15 | return ( 16 | <p className='text-md font-light'> 17 | <span>{formatQuantity(bedrooms, 'bedroom')} ·</span> 18 | <span>{formatQuantity(baths, 'bath')} ·</span> 19 | <span>{formatQuantity(guests, 'guest')} ·</span> 20 | <span>{formatQuantity(beds, 'bed')}</span> 21 | </p> 22 | ); 23 | } 24 | export default PropertyDetails; 25 | -------------------------------------------------------------------------------- /02-home-away-project/components/properties/PropertyMap.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { MapContainer, TileLayer, Marker, ZoomControl } from 'react-leaflet'; 3 | import 'leaflet/dist/leaflet.css'; 4 | import { icon } from 'leaflet'; 5 | const iconUrl = 6 | 'https://unpkg.com/leaflet@1.9.3/dist/images/marker-icon-2x.png'; 7 | const markerIcon = icon({ 8 | iconUrl: iconUrl, 9 | iconSize: [20, 30], 10 | }); 11 | 12 | import { findCountryByCode } from '@/utils/countries'; 13 | import CountryFlagAndName from '../card/CountryFlagAndName'; 14 | import Title from './Title'; 15 | 16 | function PropertyMap({ countryCode }: { countryCode: string }) { 17 | const defaultLocation = [51.505, -0.09] as [number, number]; 18 | const location = findCountryByCode(countryCode)?.location as [number, number]; 19 | 20 | return ( 21 | <div className='mt-4'> 22 | <div className='mb-4'> 23 | <Title text='Where you will be staying' /> 24 | <CountryFlagAndName countryCode={countryCode} /> 25 | </div> 26 | <MapContainer 27 | scrollWheelZoom={false} 28 | zoomControl={false} 29 | className='h-[50vh] rounded-lg relative z-0' 30 | center={location || defaultLocation} 31 | zoom={7} 32 | > 33 | <TileLayer 34 | attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' 35 | url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' 36 | /> 37 | <ZoomControl position='bottomright' /> 38 | <Marker position={location || defaultLocation} icon={markerIcon} /> 39 | </MapContainer> 40 | </div> 41 | ); 42 | } 43 | export default PropertyMap; 44 | -------------------------------------------------------------------------------- /02-home-away-project/components/properties/ShareButton.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { 3 | Popover, 4 | PopoverContent, 5 | PopoverTrigger, 6 | } from '@/components/ui/popover'; 7 | import { Button } from '../ui/button'; 8 | import { LuShare2 } from 'react-icons/lu'; 9 | 10 | import { 11 | TwitterShareButton, 12 | EmailShareButton, 13 | LinkedinShareButton, 14 | TwitterIcon, 15 | EmailIcon, 16 | LinkedinIcon, 17 | } from 'react-share'; 18 | 19 | function ShareButton({ 20 | propertyId, 21 | name, 22 | }: { 23 | propertyId: string; 24 | name: string; 25 | }) { 26 | const url = process.env.NEXT_PUBLIC_WEBSITE_URL; 27 | const shareLink = `${url}/properties/${propertyId}`; 28 | return ( 29 | <Popover> 30 | <PopoverTrigger asChild> 31 | <Button variant='outline' size='icon' className='p-2'> 32 | <LuShare2 /> 33 | </Button> 34 | </PopoverTrigger> 35 | <PopoverContent 36 | side='top' 37 | align='end' 38 | sideOffset={10} 39 | className='flex items-center gap-x-2 justify-center w-full' 40 | > 41 | <TwitterShareButton url={shareLink} title={name}> 42 | <TwitterIcon size={32} round /> 43 | </TwitterShareButton> 44 | <LinkedinShareButton url={shareLink} title={name}> 45 | <LinkedinIcon size={32} round /> 46 | </LinkedinShareButton> 47 | <EmailShareButton url={shareLink} title={name}> 48 | <EmailIcon size={32} round /> 49 | </EmailShareButton> 50 | </PopoverContent> 51 | </Popover> 52 | ); 53 | } 54 | export default ShareButton; 55 | -------------------------------------------------------------------------------- /02-home-away-project/components/properties/Title.tsx: -------------------------------------------------------------------------------- 1 | function Title({ text }: { text: string }) { 2 | return <h3 className='text-lg font-bold mb-2'>{text}</h3>; 3 | } 4 | export default Title; 5 | -------------------------------------------------------------------------------- /02-home-away-project/components/properties/UserInfo.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | 3 | type UserInfoProps = { 4 | profile: { 5 | profileImage: string; 6 | firstName: string; 7 | }; 8 | }; 9 | function UserInfo({ profile: { profileImage, firstName } }: UserInfoProps) { 10 | return ( 11 | <article className='grid grid-cols-[auto,1fr] gap-4 mt-4'> 12 | <Image 13 | src={profileImage} 14 | alt={firstName} 15 | width={50} 16 | height={50} 17 | className='rounded w-12 h-12 object-cover' 18 | /> 19 | <div> 20 | <p> 21 | Hosted by <span className='font-bold'> {firstName}</span> 22 | </p> 23 | <p className='text-muted-foreground font-light'> 24 | Superhost · 2 years hosting 25 | </p> 26 | </div> 27 | </article> 28 | ); 29 | } 30 | export default UserInfo; 31 | -------------------------------------------------------------------------------- /02-home-away-project/components/reservations/Stats.tsx: -------------------------------------------------------------------------------- 1 | import StatsCards from '@/components/admin/StatsCard'; 2 | import { fetchReservationStats } from '@/utils/actions'; 3 | import { formatCurrency } from '@/utils/format'; 4 | 5 | async function Stats() { 6 | const stats = await fetchReservationStats(); 7 | return ( 8 | <div className='mt-8 grid md:grid-cols-2 gap-4 lg:grid-cols-3'> 9 | <StatsCards title='properties' value={stats.properties} /> 10 | <StatsCards title='nights' value={stats.nights} /> 11 | <StatsCards title='amount' value={formatCurrency(stats.amount)} /> 12 | </div> 13 | ); 14 | } 15 | export default Stats; 16 | -------------------------------------------------------------------------------- /02-home-away-project/components/reviews/Comment.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { useState } from 'react'; 3 | import { Button } from '@/components/ui/button'; 4 | function Comment({ comment }: { comment: string }) { 5 | const [isExpanded, setIsExpanded] = useState(false); 6 | 7 | const toggleExpanded = () => { 8 | setIsExpanded(!isExpanded); 9 | }; 10 | const longComment = comment.length > 130; 11 | const displayComment = 12 | longComment && !isExpanded ? `${comment.slice(0, 130)}...` : comment; 13 | 14 | return ( 15 | <div> 16 | <p className='text-sm'>{displayComment}</p> 17 | {longComment && ( 18 | <Button 19 | variant='link' 20 | className='pl-0 text-muted-foreground' 21 | onClick={toggleExpanded} 22 | > 23 | {isExpanded ? 'Show Less' : 'Show More'} 24 | </Button> 25 | )} 26 | </div> 27 | ); 28 | } 29 | 30 | export default Comment; 31 | -------------------------------------------------------------------------------- /02-home-away-project/components/reviews/PropertyReviews.tsx: -------------------------------------------------------------------------------- 1 | import { fetchPropertyReviews } from '@/utils/actions'; 2 | import Title from '@/components/properties/Title'; 3 | 4 | import ReviewCard from './ReviewCard'; 5 | async function PropertyReviews({ propertyId }: { propertyId: string }) { 6 | const reviews = await fetchPropertyReviews(propertyId); 7 | if (reviews.length < 1) return null; 8 | return ( 9 | <div className='mt-8'> 10 | <Title text='Reviews' /> 11 | <div className='grid md:grid-cols-2 gap-8 mt-4 '> 12 | {reviews.map((review) => { 13 | const { comment, rating } = review; 14 | const { firstName, profileImage } = review.profile; 15 | const reviewInfo = { 16 | comment, 17 | rating, 18 | name: firstName, 19 | image: profileImage, 20 | }; 21 | return <ReviewCard key={review.id} reviewInfo={reviewInfo} />; 22 | })} 23 | </div> 24 | </div> 25 | ); 26 | } 27 | export default PropertyReviews; 28 | -------------------------------------------------------------------------------- /02-home-away-project/components/reviews/Rating.tsx: -------------------------------------------------------------------------------- 1 | import { FaStar, FaRegStar } from 'react-icons/fa'; 2 | 3 | function Rating({ rating }: { rating: number }) { 4 | // rating = 2 5 | // 1 <= 2 true 6 | // 2 <= 2 true 7 | // 3 <= 2 false 8 | // .... 9 | const stars = Array.from({ length: 5 }, (_, i) => i + 1 <= rating); 10 | 11 | return ( 12 | <div className='flex items-center gap-x-1'> 13 | {stars.map((isFilled, i) => { 14 | const className = `w-3 h-3 ${ 15 | isFilled ? 'text-primary' : 'text-gray-400' 16 | }`; 17 | return isFilled ? ( 18 | <FaStar className={className} key={i} /> 19 | ) : ( 20 | <FaRegStar className={className} key={i} /> 21 | ); 22 | })} 23 | </div> 24 | ); 25 | } 26 | 27 | export default Rating; 28 | -------------------------------------------------------------------------------- /02-home-away-project/components/reviews/ReviewCard.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardContent, CardHeader } from '@/components/ui/card'; 2 | import Rating from './Rating'; 3 | import Comment from './Comment'; 4 | type ReviewCardProps = { 5 | reviewInfo: { 6 | comment: string; 7 | rating: number; 8 | name: string; 9 | image: string; 10 | }; 11 | children?: React.ReactNode; 12 | }; 13 | 14 | function ReviewCard({ reviewInfo, children }: ReviewCardProps) { 15 | return ( 16 | <Card className='relative'> 17 | <CardHeader> 18 | <div className='flex items-center'> 19 | <img 20 | src={reviewInfo.image} 21 | alt='profile' 22 | className='w-12 h-12 rounded-full object-cover' 23 | /> 24 | <div className='ml-4'> 25 | <h3 className='text-sm font-bold capitalize mb-1'> 26 | {reviewInfo.name} 27 | </h3> 28 | <Rating rating={reviewInfo.rating} /> 29 | </div> 30 | </div> 31 | </CardHeader> 32 | <CardContent> 33 | <Comment comment={reviewInfo.comment} /> 34 | </CardContent> 35 | {/* delete button later */} 36 | <div className='absolute top-3 right-3'>{children}</div> 37 | </Card> 38 | ); 39 | } 40 | export default ReviewCard; 41 | -------------------------------------------------------------------------------- /02-home-away-project/components/reviews/SubmitReview.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { useState } from 'react'; 3 | import { SubmitButton } from '@/components/form/Buttons'; 4 | import FormContainer from '@/components/form/FormContainer'; 5 | import { Card } from '@/components/ui/card'; 6 | import RatingInput from '@/components/form/RatingInput'; 7 | import TextAreaInput from '@/components/form/TextAreaInput'; 8 | import { Button } from '@/components/ui/button'; 9 | import { createReviewAction } from '@/utils/actions'; 10 | function SubmitReview({ propertyId }: { propertyId: string }) { 11 | const [isReviewFormVisible, setIsReviewFormVisible] = useState(false); 12 | return ( 13 | <div className='mt-8'> 14 | <Button onClick={() => setIsReviewFormVisible((prev) => !prev)}> 15 | Leave a Review 16 | </Button> 17 | {isReviewFormVisible && ( 18 | <Card className='p-8 mt-8'> 19 | <FormContainer action={createReviewAction}> 20 | <input type='hidden' name='propertyId' value={propertyId} /> 21 | <RatingInput name='rating' /> 22 | <TextAreaInput 23 | name='comment' 24 | labelText='feedback' 25 | defaultValue='Amazing place !!!' 26 | /> 27 | <SubmitButton text='Submit' className='mt-4' /> 28 | </FormContainer> 29 | </Card> 30 | )} 31 | </div> 32 | ); 33 | } 34 | 35 | export default SubmitReview; 36 | -------------------------------------------------------------------------------- /02-home-away-project/components/ui/breadcrumb.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { ChevronRightIcon, DotsHorizontalIcon } from "@radix-ui/react-icons" 3 | import { Slot } from "@radix-ui/react-slot" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const Breadcrumb = React.forwardRef< 8 | HTMLElement, 9 | React.ComponentPropsWithoutRef<"nav"> & { 10 | separator?: React.ReactNode 11 | } 12 | >(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />) 13 | Breadcrumb.displayName = "Breadcrumb" 14 | 15 | const BreadcrumbList = React.forwardRef< 16 | HTMLOListElement, 17 | React.ComponentPropsWithoutRef<"ol"> 18 | >(({ className, ...props }, ref) => ( 19 | <ol 20 | ref={ref} 21 | className={cn( 22 | "flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5", 23 | className 24 | )} 25 | {...props} 26 | /> 27 | )) 28 | BreadcrumbList.displayName = "BreadcrumbList" 29 | 30 | const BreadcrumbItem = React.forwardRef< 31 | HTMLLIElement, 32 | React.ComponentPropsWithoutRef<"li"> 33 | >(({ className, ...props }, ref) => ( 34 | <li 35 | ref={ref} 36 | className={cn("inline-flex items-center gap-1.5", className)} 37 | {...props} 38 | /> 39 | )) 40 | BreadcrumbItem.displayName = "BreadcrumbItem" 41 | 42 | const BreadcrumbLink = React.forwardRef< 43 | HTMLAnchorElement, 44 | React.ComponentPropsWithoutRef<"a"> & { 45 | asChild?: boolean 46 | } 47 | >(({ asChild, className, ...props }, ref) => { 48 | const Comp = asChild ? Slot : "a" 49 | 50 | return ( 51 | <Comp 52 | ref={ref} 53 | className={cn("transition-colors hover:text-foreground", className)} 54 | {...props} 55 | /> 56 | ) 57 | }) 58 | BreadcrumbLink.displayName = "BreadcrumbLink" 59 | 60 | const BreadcrumbPage = React.forwardRef< 61 | HTMLSpanElement, 62 | React.ComponentPropsWithoutRef<"span"> 63 | >(({ className, ...props }, ref) => ( 64 | <span 65 | ref={ref} 66 | role="link" 67 | aria-disabled="true" 68 | aria-current="page" 69 | className={cn("font-normal text-foreground", className)} 70 | {...props} 71 | /> 72 | )) 73 | BreadcrumbPage.displayName = "BreadcrumbPage" 74 | 75 | const BreadcrumbSeparator = ({ 76 | children, 77 | className, 78 | ...props 79 | }: React.ComponentProps<"li">) => ( 80 | <li 81 | role="presentation" 82 | aria-hidden="true" 83 | className={cn("[&>svg]:size-3.5", className)} 84 | {...props} 85 | > 86 | {children ?? <ChevronRightIcon />} 87 | </li> 88 | ) 89 | BreadcrumbSeparator.displayName = "BreadcrumbSeparator" 90 | 91 | const BreadcrumbEllipsis = ({ 92 | className, 93 | ...props 94 | }: React.ComponentProps<"span">) => ( 95 | <span 96 | role="presentation" 97 | aria-hidden="true" 98 | className={cn("flex h-9 w-9 items-center justify-center", className)} 99 | {...props} 100 | > 101 | <DotsHorizontalIcon className="h-4 w-4" /> 102 | <span className="sr-only">More</span> 103 | </span> 104 | ) 105 | BreadcrumbEllipsis.displayName = "BreadcrumbElipssis" 106 | 107 | export { 108 | Breadcrumb, 109 | BreadcrumbList, 110 | BreadcrumbItem, 111 | BreadcrumbLink, 112 | BreadcrumbPage, 113 | BreadcrumbSeparator, 114 | BreadcrumbEllipsis, 115 | } 116 | -------------------------------------------------------------------------------- /02-home-away-project/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<HTMLButtonElement>, 39 | VariantProps<typeof buttonVariants> { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button" 46 | return ( 47 | <Comp 48 | className={cn(buttonVariants({ variant, size, className }))} 49 | ref={ref} 50 | {...props} 51 | /> 52 | ) 53 | } 54 | ) 55 | Button.displayName = "Button" 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /02-home-away-project/components/ui/calendar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons" 5 | import { DayPicker } from "react-day-picker" 6 | 7 | import { cn } from "@/lib/utils" 8 | import { buttonVariants } from "@/components/ui/button" 9 | 10 | export type CalendarProps = React.ComponentProps<typeof DayPicker> 11 | 12 | function Calendar({ 13 | className, 14 | classNames, 15 | showOutsideDays = true, 16 | ...props 17 | }: CalendarProps) { 18 | return ( 19 | <DayPicker 20 | showOutsideDays={showOutsideDays} 21 | className={cn("p-3", className)} 22 | classNames={{ 23 | months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0", 24 | month: "space-y-4", 25 | caption: "flex justify-center pt-1 relative items-center", 26 | caption_label: "text-sm font-medium", 27 | nav: "space-x-1 flex items-center", 28 | nav_button: cn( 29 | buttonVariants({ variant: "outline" }), 30 | "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100" 31 | ), 32 | nav_button_previous: "absolute left-1", 33 | nav_button_next: "absolute right-1", 34 | table: "w-full border-collapse space-y-1", 35 | head_row: "flex", 36 | head_cell: 37 | "text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]", 38 | row: "flex w-full mt-2", 39 | cell: cn( 40 | "relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md", 41 | props.mode === "range" 42 | ? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md" 43 | : "[&:has([aria-selected])]:rounded-md" 44 | ), 45 | day: cn( 46 | buttonVariants({ variant: "ghost" }), 47 | "h-8 w-8 p-0 font-normal aria-selected:opacity-100" 48 | ), 49 | day_range_start: "day-range-start", 50 | day_range_end: "day-range-end", 51 | day_selected: 52 | "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground", 53 | day_today: "bg-accent text-accent-foreground", 54 | day_outside: 55 | "day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30", 56 | day_disabled: "text-muted-foreground opacity-50", 57 | day_range_middle: 58 | "aria-selected:bg-accent aria-selected:text-accent-foreground", 59 | day_hidden: "invisible", 60 | ...classNames, 61 | }} 62 | components={{ 63 | IconLeft: ({ ...props }) => <ChevronLeftIcon className="h-4 w-4" />, 64 | IconRight: ({ ...props }) => <ChevronRightIcon className="h-4 w-4" />, 65 | }} 66 | {...props} 67 | /> 68 | ) 69 | } 70 | Calendar.displayName = "Calendar" 71 | 72 | export { Calendar } 73 | -------------------------------------------------------------------------------- /02-home-away-project/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes<HTMLDivElement> 8 | >(({ className, ...props }, ref) => ( 9 | <div 10 | ref={ref} 11 | className={cn( 12 | "rounded-xl border bg-card text-card-foreground shadow", 13 | className 14 | )} 15 | {...props} 16 | /> 17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes<HTMLDivElement> 23 | >(({ className, ...props }, ref) => ( 24 | <div 25 | ref={ref} 26 | className={cn("flex flex-col space-y-1.5 p-6", className)} 27 | {...props} 28 | /> 29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes<HTMLHeadingElement> 35 | >(({ className, ...props }, ref) => ( 36 | <h3 37 | ref={ref} 38 | className={cn("font-semibold leading-none tracking-tight", className)} 39 | {...props} 40 | /> 41 | )) 42 | CardTitle.displayName = "CardTitle" 43 | 44 | const CardDescription = React.forwardRef< 45 | HTMLParagraphElement, 46 | React.HTMLAttributes<HTMLParagraphElement> 47 | >(({ className, ...props }, ref) => ( 48 | <p 49 | ref={ref} 50 | className={cn("text-sm text-muted-foreground", className)} 51 | {...props} 52 | /> 53 | )) 54 | CardDescription.displayName = "CardDescription" 55 | 56 | const CardContent = React.forwardRef< 57 | HTMLDivElement, 58 | React.HTMLAttributes<HTMLDivElement> 59 | >(({ className, ...props }, ref) => ( 60 | <div ref={ref} className={cn("p-6 pt-0", className)} {...props} /> 61 | )) 62 | CardContent.displayName = "CardContent" 63 | 64 | const CardFooter = React.forwardRef< 65 | HTMLDivElement, 66 | React.HTMLAttributes<HTMLDivElement> 67 | >(({ className, ...props }, ref) => ( 68 | <div 69 | ref={ref} 70 | className={cn("flex items-center p-6 pt-0", className)} 71 | {...props} 72 | /> 73 | )) 74 | CardFooter.displayName = "CardFooter" 75 | 76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 77 | -------------------------------------------------------------------------------- /02-home-away-project/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox" 5 | import { CheckIcon } from "@radix-ui/react-icons" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Checkbox = React.forwardRef< 10 | React.ElementRef<typeof CheckboxPrimitive.Root>, 11 | React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> 12 | >(({ className, ...props }, ref) => ( 13 | <CheckboxPrimitive.Root 14 | ref={ref} 15 | className={cn( 16 | "peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground", 17 | className 18 | )} 19 | {...props} 20 | > 21 | <CheckboxPrimitive.Indicator 22 | className={cn("flex items-center justify-center text-current")} 23 | > 24 | <CheckIcon className="h-4 w-4" /> 25 | </CheckboxPrimitive.Indicator> 26 | </CheckboxPrimitive.Root> 27 | )) 28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName 29 | 30 | export { Checkbox } 31 | -------------------------------------------------------------------------------- /02-home-away-project/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes<HTMLInputElement> {} 7 | 8 | const Input = React.forwardRef<HTMLInputElement, InputProps>( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | <input 12 | type={type} 13 | className={cn( 14 | "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50", 15 | className 16 | )} 17 | ref={ref} 18 | {...props} 19 | /> 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /02-home-away-project/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef<typeof LabelPrimitive.Root>, 15 | React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & 16 | VariantProps<typeof labelVariants> 17 | >(({ className, ...props }, ref) => ( 18 | <LabelPrimitive.Root 19 | ref={ref} 20 | className={cn(labelVariants(), className)} 21 | {...props} 22 | /> 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /02-home-away-project/components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as PopoverPrimitive from "@radix-ui/react-popover" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Popover = PopoverPrimitive.Root 9 | 10 | const PopoverTrigger = PopoverPrimitive.Trigger 11 | 12 | const PopoverAnchor = PopoverPrimitive.Anchor 13 | 14 | const PopoverContent = React.forwardRef< 15 | React.ElementRef<typeof PopoverPrimitive.Content>, 16 | React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> 17 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 18 | <PopoverPrimitive.Portal> 19 | <PopoverPrimitive.Content 20 | ref={ref} 21 | align={align} 22 | sideOffset={sideOffset} 23 | className={cn( 24 | "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", 25 | className 26 | )} 27 | {...props} 28 | /> 29 | </PopoverPrimitive.Portal> 30 | )) 31 | PopoverContent.displayName = PopoverPrimitive.Content.displayName 32 | 33 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } 34 | -------------------------------------------------------------------------------- /02-home-away-project/components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const ScrollArea = React.forwardRef< 9 | React.ElementRef<typeof ScrollAreaPrimitive.Root>, 10 | React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> 11 | >(({ className, children, ...props }, ref) => ( 12 | <ScrollAreaPrimitive.Root 13 | ref={ref} 14 | className={cn("relative overflow-hidden", className)} 15 | {...props} 16 | > 17 | <ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]"> 18 | {children} 19 | </ScrollAreaPrimitive.Viewport> 20 | <ScrollBar /> 21 | <ScrollAreaPrimitive.Corner /> 22 | </ScrollAreaPrimitive.Root> 23 | )) 24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName 25 | 26 | const ScrollBar = React.forwardRef< 27 | React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>, 28 | React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar> 29 | >(({ className, orientation = "vertical", ...props }, ref) => ( 30 | <ScrollAreaPrimitive.ScrollAreaScrollbar 31 | ref={ref} 32 | orientation={orientation} 33 | className={cn( 34 | "flex touch-none select-none transition-colors", 35 | orientation === "vertical" && 36 | "h-full w-2.5 border-l border-l-transparent p-[1px]", 37 | orientation === "horizontal" && 38 | "h-2.5 flex-col border-t border-t-transparent p-[1px]", 39 | className 40 | )} 41 | {...props} 42 | > 43 | <ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" /> 44 | </ScrollAreaPrimitive.ScrollAreaScrollbar> 45 | )) 46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName 47 | 48 | export { ScrollArea, ScrollBar } 49 | -------------------------------------------------------------------------------- /02-home-away-project/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Separator = React.forwardRef< 9 | React.ElementRef<typeof SeparatorPrimitive.Root>, 10 | React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root> 11 | >( 12 | ( 13 | { className, orientation = "horizontal", decorative = true, ...props }, 14 | ref 15 | ) => ( 16 | <SeparatorPrimitive.Root 17 | ref={ref} 18 | decorative={decorative} 19 | orientation={orientation} 20 | className={cn( 21 | "shrink-0 bg-border", 22 | orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]", 23 | className 24 | )} 25 | {...props} 26 | /> 27 | ) 28 | ) 29 | Separator.displayName = SeparatorPrimitive.Root.displayName 30 | 31 | export { Separator } 32 | -------------------------------------------------------------------------------- /02-home-away-project/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes<HTMLDivElement>) { 7 | return ( 8 | <div 9 | className={cn("animate-pulse rounded-md bg-primary/10", className)} 10 | {...props} 11 | /> 12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /02-home-away-project/components/ui/table.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Table = React.forwardRef< 6 | HTMLTableElement, 7 | React.HTMLAttributes<HTMLTableElement> 8 | >(({ className, ...props }, ref) => ( 9 | <div className="relative w-full overflow-auto"> 10 | <table 11 | ref={ref} 12 | className={cn("w-full caption-bottom text-sm", className)} 13 | {...props} 14 | /> 15 | </div> 16 | )) 17 | Table.displayName = "Table" 18 | 19 | const TableHeader = React.forwardRef< 20 | HTMLTableSectionElement, 21 | React.HTMLAttributes<HTMLTableSectionElement> 22 | >(({ className, ...props }, ref) => ( 23 | <thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} /> 24 | )) 25 | TableHeader.displayName = "TableHeader" 26 | 27 | const TableBody = React.forwardRef< 28 | HTMLTableSectionElement, 29 | React.HTMLAttributes<HTMLTableSectionElement> 30 | >(({ className, ...props }, ref) => ( 31 | <tbody 32 | ref={ref} 33 | className={cn("[&_tr:last-child]:border-0", className)} 34 | {...props} 35 | /> 36 | )) 37 | TableBody.displayName = "TableBody" 38 | 39 | const TableFooter = React.forwardRef< 40 | HTMLTableSectionElement, 41 | React.HTMLAttributes<HTMLTableSectionElement> 42 | >(({ className, ...props }, ref) => ( 43 | <tfoot 44 | ref={ref} 45 | className={cn( 46 | "border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", 47 | className 48 | )} 49 | {...props} 50 | /> 51 | )) 52 | TableFooter.displayName = "TableFooter" 53 | 54 | const TableRow = React.forwardRef< 55 | HTMLTableRowElement, 56 | React.HTMLAttributes<HTMLTableRowElement> 57 | >(({ className, ...props }, ref) => ( 58 | <tr 59 | ref={ref} 60 | className={cn( 61 | "border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted", 62 | className 63 | )} 64 | {...props} 65 | /> 66 | )) 67 | TableRow.displayName = "TableRow" 68 | 69 | const TableHead = React.forwardRef< 70 | HTMLTableCellElement, 71 | React.ThHTMLAttributes<HTMLTableCellElement> 72 | >(({ className, ...props }, ref) => ( 73 | <th 74 | ref={ref} 75 | className={cn( 76 | "h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", 77 | className 78 | )} 79 | {...props} 80 | /> 81 | )) 82 | TableHead.displayName = "TableHead" 83 | 84 | const TableCell = React.forwardRef< 85 | HTMLTableCellElement, 86 | React.TdHTMLAttributes<HTMLTableCellElement> 87 | >(({ className, ...props }, ref) => ( 88 | <td 89 | ref={ref} 90 | className={cn( 91 | "p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", 92 | className 93 | )} 94 | {...props} 95 | /> 96 | )) 97 | TableCell.displayName = "TableCell" 98 | 99 | const TableCaption = React.forwardRef< 100 | HTMLTableCaptionElement, 101 | React.HTMLAttributes<HTMLTableCaptionElement> 102 | >(({ className, ...props }, ref) => ( 103 | <caption 104 | ref={ref} 105 | className={cn("mt-4 text-sm text-muted-foreground", className)} 106 | {...props} 107 | /> 108 | )) 109 | TableCaption.displayName = "TableCaption" 110 | 111 | export { 112 | Table, 113 | TableHeader, 114 | TableBody, 115 | TableFooter, 116 | TableHead, 117 | TableRow, 118 | TableCell, 119 | TableCaption, 120 | } 121 | -------------------------------------------------------------------------------- /02-home-away-project/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {} 7 | 8 | const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 | <textarea 12 | className={cn( 13 | "flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50", 14 | className 15 | )} 16 | ref={ref} 17 | {...props} 18 | /> 19 | ) 20 | } 21 | ) 22 | Textarea.displayName = "Textarea" 23 | 24 | export { Textarea } 25 | -------------------------------------------------------------------------------- /02-home-away-project/components/ui/toaster.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { 4 | Toast, 5 | ToastClose, 6 | ToastDescription, 7 | ToastProvider, 8 | ToastTitle, 9 | ToastViewport, 10 | } from "@/components/ui/toast" 11 | import { useToast } from "@/components/ui/use-toast" 12 | 13 | export function Toaster() { 14 | const { toasts } = useToast() 15 | 16 | return ( 17 | <ToastProvider> 18 | {toasts.map(function ({ id, title, description, action, ...props }) { 19 | return ( 20 | <Toast key={id} {...props}> 21 | <div className="grid gap-1"> 22 | {title && <ToastTitle>{title}</ToastTitle>} 23 | {description && ( 24 | <ToastDescription>{description}</ToastDescription> 25 | )} 26 | </div> 27 | {action} 28 | <ToastClose /> 29 | </Toast> 30 | ) 31 | })} 32 | <ToastViewport /> 33 | </ToastProvider> 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /02-home-away-project/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /02-home-away-project/middleware.ts: -------------------------------------------------------------------------------- 1 | import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'; 2 | import { NextResponse } from 'next/server'; 3 | 4 | const isPublicRoute = createRouteMatcher(['/', '/properties(.*)']); 5 | const isAdminRoute = createRouteMatcher(['/admin(.*)']); 6 | 7 | export default clerkMiddleware((auth, req) => { 8 | const isAdminUser = auth().userId === process.env.ADMIN_USER_ID; 9 | if (isAdminRoute(req) && !isAdminUser) { 10 | return NextResponse.redirect(new URL('/', req.url)); 11 | } 12 | if (!isPublicRoute(req)) auth().protect(); 13 | }); 14 | 15 | export const config = { 16 | matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'], 17 | }; 18 | -------------------------------------------------------------------------------- /02-home-away-project/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: 'https', 7 | hostname: 'img.clerk.com', 8 | }, 9 | { 10 | protocol: 'https', 11 | hostname: 'virmjpqxaajeqwjohjll.supabase.co', 12 | }, 13 | ], 14 | }, 15 | }; 16 | 17 | export default nextConfig; 18 | -------------------------------------------------------------------------------- /02-home-away-project/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "home-away", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "npx prisma generate && next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@clerk/nextjs": "^5.0.1", 13 | "@prisma/client": "^5.12.1", 14 | "@radix-ui/react-checkbox": "^1.0.4", 15 | "@radix-ui/react-dropdown-menu": "^2.0.6", 16 | "@radix-ui/react-icons": "^1.3.0", 17 | "@radix-ui/react-label": "^2.0.2", 18 | "@radix-ui/react-popover": "^1.0.7", 19 | "@radix-ui/react-scroll-area": "^1.0.5", 20 | "@radix-ui/react-select": "^2.0.0", 21 | "@radix-ui/react-separator": "^1.0.3", 22 | "@radix-ui/react-slot": "^1.0.2", 23 | "@radix-ui/react-toast": "^1.1.5", 24 | "@stripe/react-stripe-js": "^2.7.1", 25 | "@stripe/stripe-js": "^3.4.1", 26 | "@supabase/supabase-js": "^2.42.5", 27 | "axios": "^1.7.2", 28 | "class-variance-authority": "^0.7.0", 29 | "clsx": "^2.1.0", 30 | "date-fns": "^3.6.0", 31 | "leaflet": "^1.9.4", 32 | "next": "14.2.1", 33 | "next-themes": "^0.3.0", 34 | "react": "^18.3.1", 35 | "react-day-picker": "^8.10.0", 36 | "react-dom": "^18.3.1", 37 | "react-icons": "^5.1.0", 38 | "react-leaflet": "^4.2.1", 39 | "react-share": "^5.1.0", 40 | "recharts": "^2.12.7", 41 | "stripe": "^15.8.0", 42 | "tailwind-merge": "^2.2.2", 43 | "tailwindcss-animate": "^1.0.7", 44 | "use-debounce": "^10.0.0", 45 | "world-countries": "^5.0.0", 46 | "zod": "^3.22.4", 47 | "zustand": "^4.5.2" 48 | }, 49 | "devDependencies": { 50 | "@types/leaflet": "^1.9.12", 51 | "@types/node": "^20", 52 | "@types/react": "^18", 53 | "@types/react-dom": "^18", 54 | "eslint": "^8", 55 | "eslint-config-next": "14.2.1", 56 | "postcss": "^8", 57 | "prisma": "^5.12.1", 58 | "tailwindcss": "^3.4.1", 59 | "typescript": "^5" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /02-home-away-project/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 | -------------------------------------------------------------------------------- /02-home-away-project/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "postgresql" 3 | url = env("DATABASE_URL") 4 | directUrl = env("DIRECT_URL") 5 | } 6 | 7 | generator client { 8 | provider = "prisma-client-js" 9 | } 10 | 11 | model Profile { 12 | id String @id @default(uuid()) 13 | clerkId String @unique 14 | firstName String 15 | lastName String 16 | username String 17 | email String 18 | profileImage String 19 | createdAt DateTime @default(now()) 20 | updatedAt DateTime @updatedAt 21 | properties Property[] 22 | favorites Favorite[] 23 | reviews Review[] 24 | bookings Booking[] 25 | 26 | } 27 | 28 | model Property { 29 | id String @id @default(uuid()) 30 | name String 31 | tagline String 32 | category String 33 | image String 34 | country String 35 | description String 36 | price Int 37 | guests Int 38 | bedrooms Int 39 | beds Int 40 | baths Int 41 | amenities String 42 | createdAt DateTime @default(now()) 43 | updatedAt DateTime @updatedAt 44 | profile Profile @relation(fields: [profileId], references: [clerkId], onDelete: Cascade) 45 | profileId String 46 | favorites Favorite[] 47 | reviews Review[] 48 | bookings Booking[] 49 | } 50 | 51 | 52 | model Favorite { 53 | id String @id @default(uuid()) 54 | createdAt DateTime @default(now()) 55 | updatedAt DateTime @updatedAt 56 | 57 | profile Profile @relation(fields: [profileId], references: [clerkId], onDelete: Cascade) 58 | profileId String 59 | 60 | property Property @relation(fields: [propertyId], references: [id], onDelete: Cascade) 61 | propertyId String 62 | 63 | } 64 | 65 | 66 | model Review { 67 | id String @id @default(uuid()) 68 | profile Profile @relation(fields: [profileId], references: [clerkId], onDelete: Cascade) 69 | profileId String 70 | property Property @relation(fields: [propertyId], references: [id], onDelete: Cascade) 71 | propertyId String 72 | rating Int 73 | comment String 74 | createdAt DateTime @default(now()) 75 | updatedAt DateTime @updatedAt 76 | } 77 | 78 | model Booking { 79 | id String @id @default(uuid()) 80 | profile Profile @relation(fields: [profileId], references: [clerkId], onDelete: Cascade) 81 | profileId String 82 | property Property @relation(fields: [propertyId], references: [id], onDelete: Cascade) 83 | propertyId String 84 | orderTotal Int 85 | totalNights Int 86 | checkIn DateTime 87 | checkOut DateTime 88 | paymentStatus Boolean @default(false) 89 | createdAt DateTime @default(now()) 90 | updatedAt DateTime @updatedAt 91 | } 92 | -------------------------------------------------------------------------------- /02-home-away-project/public/images/0-big-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/nextjs-course-home-away/201f7acc32b8c794ffaeb31d6e2b1a29dd0c3ca2/02-home-away-project/public/images/0-big-image.jpg -------------------------------------------------------------------------------- /02-home-away-project/public/images/0-user-peter.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/nextjs-course-home-away/201f7acc32b8c794ffaeb31d6e2b1a29dd0c3ca2/02-home-away-project/public/images/0-user-peter.jpg -------------------------------------------------------------------------------- /02-home-away-project/public/images/0-user-susan.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/nextjs-course-home-away/201f7acc32b8c794ffaeb31d6e2b1a29dd0c3ca2/02-home-away-project/public/images/0-user-susan.jpg -------------------------------------------------------------------------------- /02-home-away-project/public/images/cabin-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/nextjs-course-home-away/201f7acc32b8c794ffaeb31d6e2b1a29dd0c3ca2/02-home-away-project/public/images/cabin-1.jpg -------------------------------------------------------------------------------- /02-home-away-project/public/images/cabin-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/nextjs-course-home-away/201f7acc32b8c794ffaeb31d6e2b1a29dd0c3ca2/02-home-away-project/public/images/cabin-2.jpg -------------------------------------------------------------------------------- /02-home-away-project/public/images/cabin-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/nextjs-course-home-away/201f7acc32b8c794ffaeb31d6e2b1a29dd0c3ca2/02-home-away-project/public/images/cabin-3.jpg -------------------------------------------------------------------------------- /02-home-away-project/public/images/cabin-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/nextjs-course-home-away/201f7acc32b8c794ffaeb31d6e2b1a29dd0c3ca2/02-home-away-project/public/images/cabin-4.jpg -------------------------------------------------------------------------------- /02-home-away-project/public/images/cabin-5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/nextjs-course-home-away/201f7acc32b8c794ffaeb31d6e2b1a29dd0c3ca2/02-home-away-project/public/images/cabin-5.jpg -------------------------------------------------------------------------------- /02-home-away-project/public/images/caravan-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/nextjs-course-home-away/201f7acc32b8c794ffaeb31d6e2b1a29dd0c3ca2/02-home-away-project/public/images/caravan-1.jpg -------------------------------------------------------------------------------- /02-home-away-project/public/images/caravan-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/nextjs-course-home-away/201f7acc32b8c794ffaeb31d6e2b1a29dd0c3ca2/02-home-away-project/public/images/caravan-2.jpg -------------------------------------------------------------------------------- /02-home-away-project/public/images/caravan-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/nextjs-course-home-away/201f7acc32b8c794ffaeb31d6e2b1a29dd0c3ca2/02-home-away-project/public/images/caravan-3.jpg -------------------------------------------------------------------------------- /02-home-away-project/public/images/caravan-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/nextjs-course-home-away/201f7acc32b8c794ffaeb31d6e2b1a29dd0c3ca2/02-home-away-project/public/images/caravan-4.jpg -------------------------------------------------------------------------------- /02-home-away-project/public/images/caravan-5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/nextjs-course-home-away/201f7acc32b8c794ffaeb31d6e2b1a29dd0c3ca2/02-home-away-project/public/images/caravan-5.jpg -------------------------------------------------------------------------------- /02-home-away-project/public/images/tent-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/nextjs-course-home-away/201f7acc32b8c794ffaeb31d6e2b1a29dd0c3ca2/02-home-away-project/public/images/tent-1.jpg -------------------------------------------------------------------------------- /02-home-away-project/public/images/tent-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/nextjs-course-home-away/201f7acc32b8c794ffaeb31d6e2b1a29dd0c3ca2/02-home-away-project/public/images/tent-2.jpg -------------------------------------------------------------------------------- /02-home-away-project/public/images/tent-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/nextjs-course-home-away/201f7acc32b8c794ffaeb31d6e2b1a29dd0c3ca2/02-home-away-project/public/images/tent-3.jpg -------------------------------------------------------------------------------- /02-home-away-project/public/images/tent-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/nextjs-course-home-away/201f7acc32b8c794ffaeb31d6e2b1a29dd0c3ca2/02-home-away-project/public/images/tent-4.jpg -------------------------------------------------------------------------------- /02-home-away-project/public/images/tent-5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/nextjs-course-home-away/201f7acc32b8c794ffaeb31d6e2b1a29dd0c3ca2/02-home-away-project/public/images/tent-5.jpg -------------------------------------------------------------------------------- /02-home-away-project/public/next.svg: -------------------------------------------------------------------------------- 1 | <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg> -------------------------------------------------------------------------------- /02-home-away-project/public/vercel.svg: -------------------------------------------------------------------------------- 1 | <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg> -------------------------------------------------------------------------------- /02-home-away-project/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 -------------------------------------------------------------------------------- /02-home-away-project/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 | -------------------------------------------------------------------------------- /02-home-away-project/utils/amenities.ts: -------------------------------------------------------------------------------- 1 | import { IconType } from 'react-icons'; 2 | export type Amenity = { 3 | name: string; 4 | icon: IconType; 5 | selected: boolean; 6 | }; 7 | import { 8 | FiCloud, 9 | FiTruck, 10 | FiZap, 11 | FiWind, 12 | FiSun, 13 | FiCoffee, 14 | FiFeather, 15 | FiAirplay, 16 | FiTrello, 17 | FiBox, 18 | FiAnchor, 19 | FiDroplet, 20 | FiMapPin, 21 | FiSunrise, 22 | FiSunset, 23 | FiMusic, 24 | FiHeadphones, 25 | FiRadio, 26 | FiFilm, 27 | FiTv, 28 | } from 'react-icons/fi'; 29 | 30 | export const amenities: Amenity[] = [ 31 | { name: 'unlimited cloud storage', icon: FiCloud, selected: false }, 32 | { name: 'VIP parking for squirrels', icon: FiTruck, selected: false }, 33 | { name: 'self-lighting fire pit', icon: FiZap, selected: false }, 34 | { 35 | name: 'bbq grill with a masterchef diploma', 36 | icon: FiWind, 37 | selected: false, 38 | }, 39 | { name: 'outdoor furniture (tree stumps)', icon: FiSun, selected: false }, 40 | { name: 'private bathroom (bushes nearby)', icon: FiCoffee, selected: false }, 41 | { name: 'hot shower (sun required)', icon: FiFeather, selected: false }, 42 | { name: 'kitchenette (aka fire pit)', icon: FiAirplay, selected: false }, 43 | { name: 'natural heating (bring a coat)', icon: FiTrello, selected: false }, 44 | { 45 | name: 'air conditioning (breeze from the west)', 46 | icon: FiBox, 47 | selected: false, 48 | }, 49 | { name: 'bed linens (leaves)', icon: FiAnchor, selected: false }, 50 | { name: 'towels (more leaves)', icon: FiDroplet, selected: false }, 51 | { 52 | name: 'picnic table (yet another tree stump)', 53 | icon: FiMapPin, 54 | selected: false, 55 | }, 56 | { name: 'hammock (two trees and a rope)', icon: FiSunrise, selected: false }, 57 | { name: 'solar power (daylight)', icon: FiSunset, selected: false }, 58 | { name: 'water supply (river a mile away)', icon: FiMusic, selected: false }, 59 | { 60 | name: 'cooking utensils (sticks and stones)', 61 | icon: FiHeadphones, 62 | selected: false, 63 | }, 64 | { name: 'cool box (hole in the ground)', icon: FiRadio, selected: false }, 65 | { name: 'lanterns (fireflies)', icon: FiFilm, selected: false }, 66 | { name: 'first aid kit (hope and prayers)', icon: FiTv, selected: false }, 67 | ]; 68 | 69 | export const conservativeAmenities: Amenity[] = [ 70 | { name: 'cloud storage', icon: FiCloud, selected: false }, 71 | { name: 'parking', icon: FiTruck, selected: false }, 72 | { name: 'fire pit', icon: FiZap, selected: false }, 73 | { name: 'bbq grill', icon: FiWind, selected: false }, 74 | { name: 'outdoor furniture', icon: FiSun, selected: false }, 75 | { name: 'private bathroom', icon: FiCoffee, selected: false }, 76 | { name: 'hot shower', icon: FiFeather, selected: false }, 77 | { name: 'kitchenette', icon: FiAirplay, selected: false }, 78 | { name: 'heating', icon: FiTrello, selected: false }, 79 | { name: 'air conditioning', icon: FiBox, selected: false }, 80 | { name: 'bed linens', icon: FiAnchor, selected: false }, 81 | { name: 'towels', icon: FiDroplet, selected: false }, 82 | { name: 'picnic table', icon: FiMapPin, selected: false }, 83 | { name: 'hammock', icon: FiSunrise, selected: false }, 84 | { name: 'solar power', icon: FiSunset, selected: false }, 85 | { name: 'water supply', icon: FiMusic, selected: false }, 86 | { name: 'cooking utensils', icon: FiHeadphones, selected: false }, 87 | { name: 'cool box', icon: FiRadio, selected: false }, 88 | { name: 'lanterns', icon: FiFilm, selected: false }, 89 | { name: 'first aid kit', icon: FiTv, selected: false }, 90 | ]; 91 | -------------------------------------------------------------------------------- /02-home-away-project/utils/calculateTotals.ts: -------------------------------------------------------------------------------- 1 | import { calculateDaysBetween } from '@/utils/calendar'; 2 | 3 | type BookingDetails = { 4 | checkIn: Date; 5 | checkOut: Date; 6 | price: number; 7 | }; 8 | 9 | export const calculateTotals = ({ 10 | checkIn, 11 | checkOut, 12 | price, 13 | }: BookingDetails) => { 14 | const totalNights = calculateDaysBetween({ checkIn, checkOut }); 15 | const subTotal = totalNights * price; 16 | const cleaning = 21; 17 | const service = 40; 18 | const tax = subTotal * 0.1; 19 | const orderTotal = subTotal + cleaning + service + tax; 20 | return { totalNights, subTotal, cleaning, service, tax, orderTotal }; 21 | }; 22 | -------------------------------------------------------------------------------- /02-home-away-project/utils/calendar.ts: -------------------------------------------------------------------------------- 1 | import { DateRange } from 'react-day-picker'; 2 | import { Booking } from '@/utils/types'; 3 | 4 | export const defaultSelected: DateRange = { 5 | from: undefined, 6 | to: undefined, 7 | }; 8 | 9 | export const generateBlockedPeriods = ({ 10 | bookings, 11 | today, 12 | }: { 13 | bookings: Booking[]; 14 | today: Date; 15 | }) => { 16 | today.setHours(0, 0, 0, 0); // Set the time to 00:00:00.000 17 | 18 | const disabledDays: DateRange[] = [ 19 | ...bookings.map((booking) => ({ 20 | from: booking.checkIn, 21 | to: booking.checkOut, 22 | })), 23 | { 24 | from: new Date(0), // This is 01 January 1970 00:00:00 UTC. 25 | to: new Date(today.getTime() - 24 * 60 * 60 * 1000), // This is yesterday. 26 | }, 27 | ]; 28 | return disabledDays; 29 | }; 30 | 31 | export const generateDateRange = (range: DateRange | undefined): string[] => { 32 | if (!range || !range.from || !range.to) return []; 33 | 34 | let currentDate = new Date(range.from); 35 | const endDate = new Date(range.to); 36 | const dateRange: string[] = []; 37 | 38 | while (currentDate <= endDate) { 39 | const dateString = currentDate.toISOString().split('T')[0]; 40 | dateRange.push(dateString); 41 | currentDate.setDate(currentDate.getDate() + 1); 42 | } 43 | 44 | return dateRange; 45 | }; 46 | 47 | export const generateDisabledDates = ( 48 | disabledDays: DateRange[] 49 | ): { [key: string]: boolean } => { 50 | if (disabledDays.length === 0) return {}; 51 | 52 | const disabledDates: { [key: string]: boolean } = {}; 53 | const today = new Date(); 54 | today.setHours(0, 0, 0, 0); // set time to 00:00:00 to compare only the date part 55 | 56 | disabledDays.forEach((range) => { 57 | if (!range.from || !range.to) return; 58 | 59 | let currentDate = new Date(range.from); 60 | const endDate = new Date(range.to); 61 | 62 | while (currentDate <= endDate) { 63 | if (currentDate < today) { 64 | currentDate.setDate(currentDate.getDate() + 1); 65 | continue; 66 | } 67 | const dateString = currentDate.toISOString().split('T')[0]; 68 | disabledDates[dateString] = true; 69 | currentDate.setDate(currentDate.getDate() + 1); 70 | } 71 | }); 72 | 73 | return disabledDates; 74 | }; 75 | 76 | export function calculateDaysBetween({ 77 | checkIn, 78 | checkOut, 79 | }: { 80 | checkIn: Date; 81 | checkOut: Date; 82 | }) { 83 | // Calculate the difference in milliseconds 84 | const diffInMs = Math.abs(checkOut.getTime() - checkIn.getTime()); 85 | 86 | // Convert the difference in milliseconds to days 87 | const diffInDays = diffInMs / (1000 * 60 * 60 * 24); 88 | 89 | return diffInDays; 90 | } 91 | -------------------------------------------------------------------------------- /02-home-away-project/utils/categories.ts: -------------------------------------------------------------------------------- 1 | import { IconType } from 'react-icons'; 2 | import { MdCabin } from 'react-icons/md'; 3 | 4 | import { TbCaravan, TbTent, TbBuildingCottage } from 'react-icons/tb'; 5 | 6 | import { GiWoodCabin, GiMushroomHouse } from 'react-icons/gi'; 7 | import { PiWarehouse, PiLighthouse, PiVan } from 'react-icons/pi'; 8 | 9 | import { GoContainer } from 'react-icons/go'; 10 | 11 | type Category = { 12 | label: CategoryLabel; 13 | icon: IconType; 14 | }; 15 | 16 | export type CategoryLabel = 17 | | 'cabin' 18 | | 'tent' 19 | | 'airstream' 20 | | 'cottage' 21 | | 'container' 22 | | 'caravan' 23 | | 'tiny' 24 | | 'magic' 25 | | 'warehouse' 26 | | 'lodge'; 27 | 28 | export const categories: Category[] = [ 29 | { 30 | label: 'cabin', 31 | icon: MdCabin, 32 | }, 33 | { 34 | label: 'airstream', 35 | icon: PiVan, 36 | }, 37 | { 38 | label: 'tent', 39 | icon: TbTent, 40 | }, 41 | { 42 | label: 'warehouse', 43 | icon: PiWarehouse, 44 | }, 45 | { 46 | label: 'cottage', 47 | icon: TbBuildingCottage, 48 | }, 49 | { 50 | label: 'magic', 51 | icon: GiMushroomHouse, 52 | }, 53 | { 54 | label: 'container', 55 | icon: GoContainer, 56 | }, 57 | { 58 | label: 'caravan', 59 | icon: TbCaravan, 60 | }, 61 | 62 | { 63 | label: 'tiny', 64 | icon: PiLighthouse, 65 | }, 66 | { 67 | label: 'lodge', 68 | icon: GiWoodCabin, 69 | }, 70 | ]; 71 | -------------------------------------------------------------------------------- /02-home-away-project/utils/countries.ts: -------------------------------------------------------------------------------- 1 | import countries from 'world-countries'; 2 | 3 | export const formattedCountries = countries.map((item) => { 4 | return { 5 | code: item.cca2, 6 | name: item.name.common, 7 | flag: item.flag, 8 | location: item.latlng, 9 | region: item.region, 10 | }; 11 | }); 12 | 13 | export const findCountryByCode = (code: string) => { 14 | return formattedCountries.find((item) => item.code === code); 15 | }; 16 | -------------------------------------------------------------------------------- /02-home-away-project/utils/db.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | 3 | const prismaClientSingleton = () => { 4 | return new PrismaClient(); 5 | }; 6 | 7 | type PrismaClientSingleton = ReturnType<typeof prismaClientSingleton>; 8 | 9 | const globalForPrisma = globalThis as unknown as { 10 | prisma: PrismaClientSingleton | undefined; 11 | }; 12 | 13 | const prisma = globalForPrisma.prisma ?? prismaClientSingleton(); 14 | 15 | export default prisma; 16 | 17 | if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma; 18 | -------------------------------------------------------------------------------- /02-home-away-project/utils/format.ts: -------------------------------------------------------------------------------- 1 | export const formatDate = (date: Date, onlyMonth?: boolean) => { 2 | const options: Intl.DateTimeFormatOptions = { 3 | year: 'numeric', 4 | month: 'long', 5 | }; 6 | if (!onlyMonth) { 7 | options.day = 'numeric'; 8 | } 9 | 10 | return new Intl.DateTimeFormat('en-US', options).format(date); 11 | }; 12 | 13 | export const formatCurrency = (amount: number | null) => { 14 | const value = amount || 0; 15 | return new Intl.NumberFormat('en-US', { 16 | style: 'currency', 17 | currency: 'USD', 18 | minimumFractionDigits: 0, 19 | maximumFractionDigits: 0, 20 | }).format(value); 21 | }; 22 | 23 | export function formatQuantity(quantity: number, noun: string): string { 24 | return quantity === 1 ? `${quantity} ${noun}` : `${quantity} ${noun}s`; 25 | } 26 | -------------------------------------------------------------------------------- /02-home-away-project/utils/links.ts: -------------------------------------------------------------------------------- 1 | type NavLink = { 2 | href: string; 3 | label: string; 4 | }; 5 | 6 | export const links: NavLink[] = [ 7 | { href: '/', label: 'home' }, 8 | { href: '/favorites ', label: 'favorites' }, 9 | { href: '/bookings ', label: 'bookings' }, 10 | { href: '/reviews ', label: 'reviews' }, 11 | { href: '/reservations ', label: 'reservations' }, 12 | { href: '/rentals/create ', label: 'create rental' }, 13 | { href: '/rentals', label: 'my rentals' }, 14 | { href: '/admin', label: 'admin' }, 15 | { href: '/profile ', label: 'profile' }, 16 | ]; 17 | -------------------------------------------------------------------------------- /02-home-away-project/utils/schemas.ts: -------------------------------------------------------------------------------- 1 | import * as z from 'zod'; 2 | import { ZodSchema } from 'zod'; 3 | 4 | export const profileSchema = z.object({ 5 | // firstName: z.string().max(5, { message: 'max length is 5' }), 6 | firstName: z.string().min(2, { 7 | message: 'first name must be at least 2 characters', 8 | }), 9 | lastName: z.string().min(2, { 10 | message: 'last name must be at least 2 characters', 11 | }), 12 | username: z.string().min(2, { 13 | message: 'username must be at least 2 characters', 14 | }), 15 | }); 16 | 17 | export function validateWithZodSchema<T>( 18 | schema: ZodSchema<T>, 19 | data: unknown 20 | ): T { 21 | const result = schema.safeParse(data); 22 | 23 | if (!result.success) { 24 | const errors = result.error.errors.map((error) => error.message); 25 | throw new Error(errors.join(',')); 26 | } 27 | return result.data; 28 | } 29 | 30 | export const imageSchema = z.object({ 31 | image: validateFile(), 32 | }); 33 | 34 | function validateFile() { 35 | const maxUploadSize = 1024 * 1024; 36 | const acceptedFilesTypes = ['image/']; 37 | return z 38 | .instanceof(File) 39 | .refine((file) => { 40 | return !file || file.size <= maxUploadSize; 41 | }, 'File size must be less than 1 MB') 42 | .refine((file) => { 43 | return ( 44 | !file || acceptedFilesTypes.some((type) => file.type.startsWith(type)) 45 | ); 46 | }, 'File must be an image'); 47 | } 48 | 49 | export const propertySchema = z.object({ 50 | name: z 51 | .string() 52 | .min(2, { 53 | message: 'name must be at least 2 characters.', 54 | }) 55 | .max(100, { 56 | message: 'name must be less than 100 characters.', 57 | }), 58 | tagline: z 59 | .string() 60 | .min(2, { 61 | message: 'tagline must be at least 2 characters.', 62 | }) 63 | .max(100, { 64 | message: 'tagline must be less than 100 characters.', 65 | }), 66 | price: z.coerce.number().int().min(0, { 67 | message: 'price must be a positive number.', 68 | }), 69 | category: z.string(), 70 | description: z.string().refine( 71 | (description) => { 72 | const wordCount = description.split(' ').length; 73 | return wordCount >= 10 && wordCount <= 1000; 74 | }, 75 | { 76 | message: 'description must be between 10 and 1000 words.', 77 | } 78 | ), 79 | country: z.string(), 80 | guests: z.coerce.number().int().min(0, { 81 | message: 'guest amount must be a positive number.', 82 | }), 83 | bedrooms: z.coerce.number().int().min(0, { 84 | message: 'bedrooms amount must be a positive number.', 85 | }), 86 | beds: z.coerce.number().int().min(0, { 87 | message: 'beds amount must be a positive number.', 88 | }), 89 | baths: z.coerce.number().int().min(0, { 90 | message: 'bahts amount must be a positive number.', 91 | }), 92 | amenities: z.string(), 93 | }); 94 | 95 | export const createReviewSchema = z.object({ 96 | propertyId: z.string(), 97 | rating: z.coerce.number().int().min(1).max(5), 98 | comment: z.string().min(10).max(1000), 99 | }); 100 | -------------------------------------------------------------------------------- /02-home-away-project/utils/store.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | import { Booking } from './types'; 3 | import { DateRange } from 'react-day-picker'; 4 | // Define the state's shape 5 | type PropertyState = { 6 | propertyId: string; 7 | price: number; 8 | bookings: Booking[]; 9 | range: DateRange | undefined; 10 | }; 11 | 12 | // Create the store 13 | export const useProperty = create<PropertyState>(() => { 14 | return { 15 | propertyId: '', 16 | price: 0, 17 | bookings: [], 18 | range: undefined, 19 | }; 20 | }); 21 | -------------------------------------------------------------------------------- /02-home-away-project/utils/supabase.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '@supabase/supabase-js'; 2 | 3 | const bucket = 'home-away-draft'; 4 | 5 | const url = process.env.SUPABASE_URL as string; 6 | const key = process.env.SUPABASE_KEY as string; 7 | 8 | const supabase = createClient(url, key); 9 | 10 | export const uploadImage = async (image: File) => { 11 | const timestamp = Date.now(); 12 | const newName = `${timestamp}-${image.name}`; 13 | const { data } = await supabase.storage 14 | .from(bucket) 15 | .upload(newName, image, { cacheControl: '3600' }); 16 | if (!data) throw new Error('Image upload failed'); 17 | return supabase.storage.from(bucket).getPublicUrl(newName).data.publicUrl; 18 | }; 19 | -------------------------------------------------------------------------------- /02-home-away-project/utils/types.ts: -------------------------------------------------------------------------------- 1 | export type actionFunction = ( 2 | prevState: any, 3 | formData: FormData 4 | ) => Promise<{ message: string }>; 5 | 6 | export type PropertyCardProps = { 7 | image: string; 8 | id: string; 9 | name: string; 10 | tagline: string; 11 | country: string; 12 | price: number; 13 | }; 14 | 15 | export type DateRangeSelect = { 16 | startDate: Date; 17 | endDate: Date; 18 | key: string; 19 | }; 20 | 21 | export type Booking = { 22 | checkIn: Date; 23 | checkOut: Date; 24 | }; 25 | -------------------------------------------------------------------------------- /03-starter/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /03-starter/.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 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /03-starter/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/nextjs-course-home-away/201f7acc32b8c794ffaeb31d6e2b1a29dd0c3ca2/03-starter/app/favicon.ico -------------------------------------------------------------------------------- /03-starter/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer components { 6 | .container { 7 | @apply mx-auto max-w-6xl xl:max-w-7xl px-8; 8 | } 9 | } 10 | 11 | @layer base { 12 | :root { 13 | --background: 0 0% 100%; 14 | --foreground: 20 14.3% 4.1%; 15 | --card: 0 0% 100%; 16 | --card-foreground: 20 14.3% 4.1%; 17 | --popover: 0 0% 100%; 18 | --popover-foreground: 20 14.3% 4.1%; 19 | --primary: 24.6 95% 53.1%; 20 | --primary-foreground: 60 9.1% 97.8%; 21 | --secondary: 60 4.8% 95.9%; 22 | --secondary-foreground: 24 9.8% 10%; 23 | --muted: 60 4.8% 95.9%; 24 | --muted-foreground: 25 5.3% 44.7%; 25 | --accent: 60 4.8% 95.9%; 26 | --accent-foreground: 24 9.8% 10%; 27 | --destructive: 0 84.2% 60.2%; 28 | --destructive-foreground: 60 9.1% 97.8%; 29 | --border: 20 5.9% 90%; 30 | --input: 20 5.9% 90%; 31 | --ring: 24.6 95% 53.1%; 32 | --radius: 0.5rem; 33 | } 34 | 35 | .dark { 36 | --background: 20 14.3% 4.1%; 37 | --foreground: 60 9.1% 97.8%; 38 | --card: 20 14.3% 4.1%; 39 | --card-foreground: 60 9.1% 97.8%; 40 | --popover: 20 14.3% 4.1%; 41 | --popover-foreground: 60 9.1% 97.8%; 42 | --primary: 20.5 90.2% 48.2%; 43 | --primary-foreground: 60 9.1% 97.8%; 44 | --secondary: 12 6.5% 15.1%; 45 | --secondary-foreground: 60 9.1% 97.8%; 46 | --muted: 12 6.5% 15.1%; 47 | --muted-foreground: 24 5.4% 63.9%; 48 | --accent: 12 6.5% 15.1%; 49 | --accent-foreground: 60 9.1% 97.8%; 50 | --destructive: 0 72.2% 50.6%; 51 | --destructive-foreground: 60 9.1% 97.8%; 52 | --border: 12 6.5% 15.1%; 53 | --input: 12 6.5% 15.1%; 54 | --ring: 20.5 90.2% 48.2%; 55 | } 56 | } 57 | 58 | @layer base { 59 | * { 60 | @apply border-border; 61 | } 62 | body { 63 | @apply bg-background text-foreground; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /03-starter/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | import { Inter } from 'next/font/google'; 3 | import './globals.css'; 4 | const inter = Inter({ subsets: ['latin'] }); 5 | 6 | export const metadata: Metadata = { 7 | title: 'HomeAway Draft', 8 | description: 'Feel at home, away from home.', 9 | }; 10 | 11 | export default function RootLayout({ 12 | children, 13 | }: Readonly<{ 14 | children: React.ReactNode; 15 | }>) { 16 | return ( 17 | <html lang='en' suppressHydrationWarning> 18 | <body className={inter.className}> 19 | <main className='container py-10'>{children}</main> 20 | </body> 21 | </html> 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /03-starter/app/page.tsx: -------------------------------------------------------------------------------- 1 | const HomePage = () => { 2 | return <h1 className='text-3xl'>HomeAway Project - Starter</h1>; 3 | }; 4 | export default HomePage; 5 | -------------------------------------------------------------------------------- /03-starter/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": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /03-starter/components/ui/breadcrumb.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { ChevronRightIcon, DotsHorizontalIcon } from "@radix-ui/react-icons" 3 | import { Slot } from "@radix-ui/react-slot" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const Breadcrumb = React.forwardRef< 8 | HTMLElement, 9 | React.ComponentPropsWithoutRef<"nav"> & { 10 | separator?: React.ReactNode 11 | } 12 | >(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />) 13 | Breadcrumb.displayName = "Breadcrumb" 14 | 15 | const BreadcrumbList = React.forwardRef< 16 | HTMLOListElement, 17 | React.ComponentPropsWithoutRef<"ol"> 18 | >(({ className, ...props }, ref) => ( 19 | <ol 20 | ref={ref} 21 | className={cn( 22 | "flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5", 23 | className 24 | )} 25 | {...props} 26 | /> 27 | )) 28 | BreadcrumbList.displayName = "BreadcrumbList" 29 | 30 | const BreadcrumbItem = React.forwardRef< 31 | HTMLLIElement, 32 | React.ComponentPropsWithoutRef<"li"> 33 | >(({ className, ...props }, ref) => ( 34 | <li 35 | ref={ref} 36 | className={cn("inline-flex items-center gap-1.5", className)} 37 | {...props} 38 | /> 39 | )) 40 | BreadcrumbItem.displayName = "BreadcrumbItem" 41 | 42 | const BreadcrumbLink = React.forwardRef< 43 | HTMLAnchorElement, 44 | React.ComponentPropsWithoutRef<"a"> & { 45 | asChild?: boolean 46 | } 47 | >(({ asChild, className, ...props }, ref) => { 48 | const Comp = asChild ? Slot : "a" 49 | 50 | return ( 51 | <Comp 52 | ref={ref} 53 | className={cn("transition-colors hover:text-foreground", className)} 54 | {...props} 55 | /> 56 | ) 57 | }) 58 | BreadcrumbLink.displayName = "BreadcrumbLink" 59 | 60 | const BreadcrumbPage = React.forwardRef< 61 | HTMLSpanElement, 62 | React.ComponentPropsWithoutRef<"span"> 63 | >(({ className, ...props }, ref) => ( 64 | <span 65 | ref={ref} 66 | role="link" 67 | aria-disabled="true" 68 | aria-current="page" 69 | className={cn("font-normal text-foreground", className)} 70 | {...props} 71 | /> 72 | )) 73 | BreadcrumbPage.displayName = "BreadcrumbPage" 74 | 75 | const BreadcrumbSeparator = ({ 76 | children, 77 | className, 78 | ...props 79 | }: React.ComponentProps<"li">) => ( 80 | <li 81 | role="presentation" 82 | aria-hidden="true" 83 | className={cn("[&>svg]:size-3.5", className)} 84 | {...props} 85 | > 86 | {children ?? <ChevronRightIcon />} 87 | </li> 88 | ) 89 | BreadcrumbSeparator.displayName = "BreadcrumbSeparator" 90 | 91 | const BreadcrumbEllipsis = ({ 92 | className, 93 | ...props 94 | }: React.ComponentProps<"span">) => ( 95 | <span 96 | role="presentation" 97 | aria-hidden="true" 98 | className={cn("flex h-9 w-9 items-center justify-center", className)} 99 | {...props} 100 | > 101 | <DotsHorizontalIcon className="h-4 w-4" /> 102 | <span className="sr-only">More</span> 103 | </span> 104 | ) 105 | BreadcrumbEllipsis.displayName = "BreadcrumbElipssis" 106 | 107 | export { 108 | Breadcrumb, 109 | BreadcrumbList, 110 | BreadcrumbItem, 111 | BreadcrumbLink, 112 | BreadcrumbPage, 113 | BreadcrumbSeparator, 114 | BreadcrumbEllipsis, 115 | } 116 | -------------------------------------------------------------------------------- /03-starter/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<HTMLButtonElement>, 39 | VariantProps<typeof buttonVariants> { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button" 46 | return ( 47 | <Comp 48 | className={cn(buttonVariants({ variant, size, className }))} 49 | ref={ref} 50 | {...props} 51 | /> 52 | ) 53 | } 54 | ) 55 | Button.displayName = "Button" 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /03-starter/components/ui/calendar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons" 5 | import { DayPicker } from "react-day-picker" 6 | 7 | import { cn } from "@/lib/utils" 8 | import { buttonVariants } from "@/components/ui/button" 9 | 10 | export type CalendarProps = React.ComponentProps<typeof DayPicker> 11 | 12 | function Calendar({ 13 | className, 14 | classNames, 15 | showOutsideDays = true, 16 | ...props 17 | }: CalendarProps) { 18 | return ( 19 | <DayPicker 20 | showOutsideDays={showOutsideDays} 21 | className={cn("p-3", className)} 22 | classNames={{ 23 | months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0", 24 | month: "space-y-4", 25 | caption: "flex justify-center pt-1 relative items-center", 26 | caption_label: "text-sm font-medium", 27 | nav: "space-x-1 flex items-center", 28 | nav_button: cn( 29 | buttonVariants({ variant: "outline" }), 30 | "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100" 31 | ), 32 | nav_button_previous: "absolute left-1", 33 | nav_button_next: "absolute right-1", 34 | table: "w-full border-collapse space-y-1", 35 | head_row: "flex", 36 | head_cell: 37 | "text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]", 38 | row: "flex w-full mt-2", 39 | cell: cn( 40 | "relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md", 41 | props.mode === "range" 42 | ? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md" 43 | : "[&:has([aria-selected])]:rounded-md" 44 | ), 45 | day: cn( 46 | buttonVariants({ variant: "ghost" }), 47 | "h-8 w-8 p-0 font-normal aria-selected:opacity-100" 48 | ), 49 | day_range_start: "day-range-start", 50 | day_range_end: "day-range-end", 51 | day_selected: 52 | "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground", 53 | day_today: "bg-accent text-accent-foreground", 54 | day_outside: 55 | "day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30", 56 | day_disabled: "text-muted-foreground opacity-50", 57 | day_range_middle: 58 | "aria-selected:bg-accent aria-selected:text-accent-foreground", 59 | day_hidden: "invisible", 60 | ...classNames, 61 | }} 62 | components={{ 63 | IconLeft: ({ ...props }) => <ChevronLeftIcon className="h-4 w-4" />, 64 | IconRight: ({ ...props }) => <ChevronRightIcon className="h-4 w-4" />, 65 | }} 66 | {...props} 67 | /> 68 | ) 69 | } 70 | Calendar.displayName = "Calendar" 71 | 72 | export { Calendar } 73 | -------------------------------------------------------------------------------- /03-starter/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes<HTMLDivElement> 8 | >(({ className, ...props }, ref) => ( 9 | <div 10 | ref={ref} 11 | className={cn( 12 | "rounded-xl border bg-card text-card-foreground shadow", 13 | className 14 | )} 15 | {...props} 16 | /> 17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes<HTMLDivElement> 23 | >(({ className, ...props }, ref) => ( 24 | <div 25 | ref={ref} 26 | className={cn("flex flex-col space-y-1.5 p-6", className)} 27 | {...props} 28 | /> 29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes<HTMLHeadingElement> 35 | >(({ className, ...props }, ref) => ( 36 | <h3 37 | ref={ref} 38 | className={cn("font-semibold leading-none tracking-tight", className)} 39 | {...props} 40 | /> 41 | )) 42 | CardTitle.displayName = "CardTitle" 43 | 44 | const CardDescription = React.forwardRef< 45 | HTMLParagraphElement, 46 | React.HTMLAttributes<HTMLParagraphElement> 47 | >(({ className, ...props }, ref) => ( 48 | <p 49 | ref={ref} 50 | className={cn("text-sm text-muted-foreground", className)} 51 | {...props} 52 | /> 53 | )) 54 | CardDescription.displayName = "CardDescription" 55 | 56 | const CardContent = React.forwardRef< 57 | HTMLDivElement, 58 | React.HTMLAttributes<HTMLDivElement> 59 | >(({ className, ...props }, ref) => ( 60 | <div ref={ref} className={cn("p-6 pt-0", className)} {...props} /> 61 | )) 62 | CardContent.displayName = "CardContent" 63 | 64 | const CardFooter = React.forwardRef< 65 | HTMLDivElement, 66 | React.HTMLAttributes<HTMLDivElement> 67 | >(({ className, ...props }, ref) => ( 68 | <div 69 | ref={ref} 70 | className={cn("flex items-center p-6 pt-0", className)} 71 | {...props} 72 | /> 73 | )) 74 | CardFooter.displayName = "CardFooter" 75 | 76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 77 | -------------------------------------------------------------------------------- /03-starter/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox" 5 | import { CheckIcon } from "@radix-ui/react-icons" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Checkbox = React.forwardRef< 10 | React.ElementRef<typeof CheckboxPrimitive.Root>, 11 | React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> 12 | >(({ className, ...props }, ref) => ( 13 | <CheckboxPrimitive.Root 14 | ref={ref} 15 | className={cn( 16 | "peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground", 17 | className 18 | )} 19 | {...props} 20 | > 21 | <CheckboxPrimitive.Indicator 22 | className={cn("flex items-center justify-center text-current")} 23 | > 24 | <CheckIcon className="h-4 w-4" /> 25 | </CheckboxPrimitive.Indicator> 26 | </CheckboxPrimitive.Root> 27 | )) 28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName 29 | 30 | export { Checkbox } 31 | -------------------------------------------------------------------------------- /03-starter/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes<HTMLInputElement> {} 7 | 8 | const Input = React.forwardRef<HTMLInputElement, InputProps>( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | <input 12 | type={type} 13 | className={cn( 14 | "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50", 15 | className 16 | )} 17 | ref={ref} 18 | {...props} 19 | /> 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /03-starter/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef<typeof LabelPrimitive.Root>, 15 | React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & 16 | VariantProps<typeof labelVariants> 17 | >(({ className, ...props }, ref) => ( 18 | <LabelPrimitive.Root 19 | ref={ref} 20 | className={cn(labelVariants(), className)} 21 | {...props} 22 | /> 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /03-starter/components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as PopoverPrimitive from "@radix-ui/react-popover" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Popover = PopoverPrimitive.Root 9 | 10 | const PopoverTrigger = PopoverPrimitive.Trigger 11 | 12 | const PopoverAnchor = PopoverPrimitive.Anchor 13 | 14 | const PopoverContent = React.forwardRef< 15 | React.ElementRef<typeof PopoverPrimitive.Content>, 16 | React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> 17 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 18 | <PopoverPrimitive.Portal> 19 | <PopoverPrimitive.Content 20 | ref={ref} 21 | align={align} 22 | sideOffset={sideOffset} 23 | className={cn( 24 | "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", 25 | className 26 | )} 27 | {...props} 28 | /> 29 | </PopoverPrimitive.Portal> 30 | )) 31 | PopoverContent.displayName = PopoverPrimitive.Content.displayName 32 | 33 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } 34 | -------------------------------------------------------------------------------- /03-starter/components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const ScrollArea = React.forwardRef< 9 | React.ElementRef<typeof ScrollAreaPrimitive.Root>, 10 | React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> 11 | >(({ className, children, ...props }, ref) => ( 12 | <ScrollAreaPrimitive.Root 13 | ref={ref} 14 | className={cn("relative overflow-hidden", className)} 15 | {...props} 16 | > 17 | <ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]"> 18 | {children} 19 | </ScrollAreaPrimitive.Viewport> 20 | <ScrollBar /> 21 | <ScrollAreaPrimitive.Corner /> 22 | </ScrollAreaPrimitive.Root> 23 | )) 24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName 25 | 26 | const ScrollBar = React.forwardRef< 27 | React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>, 28 | React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar> 29 | >(({ className, orientation = "vertical", ...props }, ref) => ( 30 | <ScrollAreaPrimitive.ScrollAreaScrollbar 31 | ref={ref} 32 | orientation={orientation} 33 | className={cn( 34 | "flex touch-none select-none transition-colors", 35 | orientation === "vertical" && 36 | "h-full w-2.5 border-l border-l-transparent p-[1px]", 37 | orientation === "horizontal" && 38 | "h-2.5 flex-col border-t border-t-transparent p-[1px]", 39 | className 40 | )} 41 | {...props} 42 | > 43 | <ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" /> 44 | </ScrollAreaPrimitive.ScrollAreaScrollbar> 45 | )) 46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName 47 | 48 | export { ScrollArea, ScrollBar } 49 | -------------------------------------------------------------------------------- /03-starter/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Separator = React.forwardRef< 9 | React.ElementRef<typeof SeparatorPrimitive.Root>, 10 | React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root> 11 | >( 12 | ( 13 | { className, orientation = "horizontal", decorative = true, ...props }, 14 | ref 15 | ) => ( 16 | <SeparatorPrimitive.Root 17 | ref={ref} 18 | decorative={decorative} 19 | orientation={orientation} 20 | className={cn( 21 | "shrink-0 bg-border", 22 | orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]", 23 | className 24 | )} 25 | {...props} 26 | /> 27 | ) 28 | ) 29 | Separator.displayName = SeparatorPrimitive.Root.displayName 30 | 31 | export { Separator } 32 | -------------------------------------------------------------------------------- /03-starter/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes<HTMLDivElement>) { 7 | return ( 8 | <div 9 | className={cn("animate-pulse rounded-md bg-primary/10", className)} 10 | {...props} 11 | /> 12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /03-starter/components/ui/table.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Table = React.forwardRef< 6 | HTMLTableElement, 7 | React.HTMLAttributes<HTMLTableElement> 8 | >(({ className, ...props }, ref) => ( 9 | <div className="relative w-full overflow-auto"> 10 | <table 11 | ref={ref} 12 | className={cn("w-full caption-bottom text-sm", className)} 13 | {...props} 14 | /> 15 | </div> 16 | )) 17 | Table.displayName = "Table" 18 | 19 | const TableHeader = React.forwardRef< 20 | HTMLTableSectionElement, 21 | React.HTMLAttributes<HTMLTableSectionElement> 22 | >(({ className, ...props }, ref) => ( 23 | <thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} /> 24 | )) 25 | TableHeader.displayName = "TableHeader" 26 | 27 | const TableBody = React.forwardRef< 28 | HTMLTableSectionElement, 29 | React.HTMLAttributes<HTMLTableSectionElement> 30 | >(({ className, ...props }, ref) => ( 31 | <tbody 32 | ref={ref} 33 | className={cn("[&_tr:last-child]:border-0", className)} 34 | {...props} 35 | /> 36 | )) 37 | TableBody.displayName = "TableBody" 38 | 39 | const TableFooter = React.forwardRef< 40 | HTMLTableSectionElement, 41 | React.HTMLAttributes<HTMLTableSectionElement> 42 | >(({ className, ...props }, ref) => ( 43 | <tfoot 44 | ref={ref} 45 | className={cn( 46 | "border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", 47 | className 48 | )} 49 | {...props} 50 | /> 51 | )) 52 | TableFooter.displayName = "TableFooter" 53 | 54 | const TableRow = React.forwardRef< 55 | HTMLTableRowElement, 56 | React.HTMLAttributes<HTMLTableRowElement> 57 | >(({ className, ...props }, ref) => ( 58 | <tr 59 | ref={ref} 60 | className={cn( 61 | "border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted", 62 | className 63 | )} 64 | {...props} 65 | /> 66 | )) 67 | TableRow.displayName = "TableRow" 68 | 69 | const TableHead = React.forwardRef< 70 | HTMLTableCellElement, 71 | React.ThHTMLAttributes<HTMLTableCellElement> 72 | >(({ className, ...props }, ref) => ( 73 | <th 74 | ref={ref} 75 | className={cn( 76 | "h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", 77 | className 78 | )} 79 | {...props} 80 | /> 81 | )) 82 | TableHead.displayName = "TableHead" 83 | 84 | const TableCell = React.forwardRef< 85 | HTMLTableCellElement, 86 | React.TdHTMLAttributes<HTMLTableCellElement> 87 | >(({ className, ...props }, ref) => ( 88 | <td 89 | ref={ref} 90 | className={cn( 91 | "p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", 92 | className 93 | )} 94 | {...props} 95 | /> 96 | )) 97 | TableCell.displayName = "TableCell" 98 | 99 | const TableCaption = React.forwardRef< 100 | HTMLTableCaptionElement, 101 | React.HTMLAttributes<HTMLTableCaptionElement> 102 | >(({ className, ...props }, ref) => ( 103 | <caption 104 | ref={ref} 105 | className={cn("mt-4 text-sm text-muted-foreground", className)} 106 | {...props} 107 | /> 108 | )) 109 | TableCaption.displayName = "TableCaption" 110 | 111 | export { 112 | Table, 113 | TableHeader, 114 | TableBody, 115 | TableFooter, 116 | TableHead, 117 | TableRow, 118 | TableCell, 119 | TableCaption, 120 | } 121 | -------------------------------------------------------------------------------- /03-starter/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {} 7 | 8 | const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 | <textarea 12 | className={cn( 13 | "flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50", 14 | className 15 | )} 16 | ref={ref} 17 | {...props} 18 | /> 19 | ) 20 | } 21 | ) 22 | Textarea.displayName = "Textarea" 23 | 24 | export { Textarea } 25 | -------------------------------------------------------------------------------- /03-starter/components/ui/toaster.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { 4 | Toast, 5 | ToastClose, 6 | ToastDescription, 7 | ToastProvider, 8 | ToastTitle, 9 | ToastViewport, 10 | } from "@/components/ui/toast" 11 | import { useToast } from "@/components/ui/use-toast" 12 | 13 | export function Toaster() { 14 | const { toasts } = useToast() 15 | 16 | return ( 17 | <ToastProvider> 18 | {toasts.map(function ({ id, title, description, action, ...props }) { 19 | return ( 20 | <Toast key={id} {...props}> 21 | <div className="grid gap-1"> 22 | {title && <ToastTitle>{title}</ToastTitle>} 23 | {description && ( 24 | <ToastDescription>{description}</ToastDescription> 25 | )} 26 | </div> 27 | {action} 28 | <ToastClose /> 29 | </Toast> 30 | ) 31 | })} 32 | <ToastViewport /> 33 | </ToastProvider> 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /03-starter/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: 'https', 7 | hostname: 'img.clerk.com', 8 | }, 9 | { 10 | protocol: 'https', 11 | hostname: 'virmjpqxaajeqwjohjll.supabase.co', 12 | }, 13 | ], 14 | }, 15 | }; 16 | 17 | export default nextConfig; 18 | -------------------------------------------------------------------------------- /03-starter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "home-away", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "npx prisma generate && next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@clerk/nextjs": "^5.0.1", 13 | "@prisma/client": "^5.12.1", 14 | "@radix-ui/react-checkbox": "^1.0.4", 15 | "@radix-ui/react-dropdown-menu": "^2.0.6", 16 | "@radix-ui/react-icons": "^1.3.0", 17 | "@radix-ui/react-label": "^2.0.2", 18 | "@radix-ui/react-popover": "^1.0.7", 19 | "@radix-ui/react-scroll-area": "^1.0.5", 20 | "@radix-ui/react-select": "^2.0.0", 21 | "@radix-ui/react-separator": "^1.0.3", 22 | "@radix-ui/react-slot": "^1.0.2", 23 | "@radix-ui/react-toast": "^1.1.5", 24 | "@stripe/react-stripe-js": "^2.7.1", 25 | "@stripe/stripe-js": "^3.4.1", 26 | "@supabase/supabase-js": "^2.42.5", 27 | "axios": "^1.7.2", 28 | "class-variance-authority": "^0.7.0", 29 | "clsx": "^2.1.0", 30 | "date-fns": "^3.6.0", 31 | "leaflet": "^1.9.4", 32 | "next": "14.2.1", 33 | "next-themes": "^0.3.0", 34 | "react": "^18.3.1", 35 | "react-day-picker": "^8.10.0", 36 | "react-dom": "^18.3.1", 37 | "react-icons": "^5.1.0", 38 | "react-leaflet": "^4.2.1", 39 | "react-share": "^5.1.0", 40 | "recharts": "^2.12.7", 41 | "stripe": "^15.8.0", 42 | "tailwind-merge": "^2.2.2", 43 | "tailwindcss-animate": "^1.0.7", 44 | "use-debounce": "^10.0.0", 45 | "world-countries": "^5.0.0", 46 | "zod": "^3.22.4", 47 | "zustand": "^4.5.2" 48 | }, 49 | "devDependencies": { 50 | "@types/leaflet": "^1.9.12", 51 | "@types/node": "^20", 52 | "@types/react": "^18", 53 | "@types/react-dom": "^18", 54 | "eslint": "^8", 55 | "eslint-config-next": "14.2.1", 56 | "postcss": "^8", 57 | "prisma": "^5.12.1", 58 | "tailwindcss": "^3.4.1", 59 | "typescript": "^5" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /03-starter/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 | -------------------------------------------------------------------------------- /03-starter/public/images/0-big-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/nextjs-course-home-away/201f7acc32b8c794ffaeb31d6e2b1a29dd0c3ca2/03-starter/public/images/0-big-image.jpg -------------------------------------------------------------------------------- /03-starter/public/images/0-user-peter.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/nextjs-course-home-away/201f7acc32b8c794ffaeb31d6e2b1a29dd0c3ca2/03-starter/public/images/0-user-peter.jpg -------------------------------------------------------------------------------- /03-starter/public/images/0-user-susan.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/nextjs-course-home-away/201f7acc32b8c794ffaeb31d6e2b1a29dd0c3ca2/03-starter/public/images/0-user-susan.jpg -------------------------------------------------------------------------------- /03-starter/public/images/cabin-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/nextjs-course-home-away/201f7acc32b8c794ffaeb31d6e2b1a29dd0c3ca2/03-starter/public/images/cabin-1.jpg -------------------------------------------------------------------------------- /03-starter/public/images/cabin-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/nextjs-course-home-away/201f7acc32b8c794ffaeb31d6e2b1a29dd0c3ca2/03-starter/public/images/cabin-2.jpg -------------------------------------------------------------------------------- /03-starter/public/images/cabin-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/nextjs-course-home-away/201f7acc32b8c794ffaeb31d6e2b1a29dd0c3ca2/03-starter/public/images/cabin-3.jpg -------------------------------------------------------------------------------- /03-starter/public/images/cabin-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/nextjs-course-home-away/201f7acc32b8c794ffaeb31d6e2b1a29dd0c3ca2/03-starter/public/images/cabin-4.jpg -------------------------------------------------------------------------------- /03-starter/public/images/cabin-5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/nextjs-course-home-away/201f7acc32b8c794ffaeb31d6e2b1a29dd0c3ca2/03-starter/public/images/cabin-5.jpg -------------------------------------------------------------------------------- /03-starter/public/images/caravan-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/nextjs-course-home-away/201f7acc32b8c794ffaeb31d6e2b1a29dd0c3ca2/03-starter/public/images/caravan-1.jpg -------------------------------------------------------------------------------- /03-starter/public/images/caravan-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/nextjs-course-home-away/201f7acc32b8c794ffaeb31d6e2b1a29dd0c3ca2/03-starter/public/images/caravan-2.jpg -------------------------------------------------------------------------------- /03-starter/public/images/caravan-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/nextjs-course-home-away/201f7acc32b8c794ffaeb31d6e2b1a29dd0c3ca2/03-starter/public/images/caravan-3.jpg -------------------------------------------------------------------------------- /03-starter/public/images/caravan-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/nextjs-course-home-away/201f7acc32b8c794ffaeb31d6e2b1a29dd0c3ca2/03-starter/public/images/caravan-4.jpg -------------------------------------------------------------------------------- /03-starter/public/images/caravan-5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/nextjs-course-home-away/201f7acc32b8c794ffaeb31d6e2b1a29dd0c3ca2/03-starter/public/images/caravan-5.jpg -------------------------------------------------------------------------------- /03-starter/public/images/tent-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/nextjs-course-home-away/201f7acc32b8c794ffaeb31d6e2b1a29dd0c3ca2/03-starter/public/images/tent-1.jpg -------------------------------------------------------------------------------- /03-starter/public/images/tent-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/nextjs-course-home-away/201f7acc32b8c794ffaeb31d6e2b1a29dd0c3ca2/03-starter/public/images/tent-2.jpg -------------------------------------------------------------------------------- /03-starter/public/images/tent-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/nextjs-course-home-away/201f7acc32b8c794ffaeb31d6e2b1a29dd0c3ca2/03-starter/public/images/tent-3.jpg -------------------------------------------------------------------------------- /03-starter/public/images/tent-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/nextjs-course-home-away/201f7acc32b8c794ffaeb31d6e2b1a29dd0c3ca2/03-starter/public/images/tent-4.jpg -------------------------------------------------------------------------------- /03-starter/public/images/tent-5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/nextjs-course-home-away/201f7acc32b8c794ffaeb31d6e2b1a29dd0c3ca2/03-starter/public/images/tent-5.jpg -------------------------------------------------------------------------------- /03-starter/public/next.svg: -------------------------------------------------------------------------------- 1 | <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg> -------------------------------------------------------------------------------- /03-starter/public/vercel.svg: -------------------------------------------------------------------------------- 1 | <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg> -------------------------------------------------------------------------------- /03-starter/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 -------------------------------------------------------------------------------- /03-starter/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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Start your path to becoming a proficient web developer with our detailed video course on building apps using Next JS 14. Designed specifically for beginners and intermediate developers, this course will help you develop high-level skills. Begin by creating a Next.js application from scratch, understanding its structure, and mastering advanced routing techniques, including link components and dynamic paths. 2 | 3 | Delve into front-end design using TailwindCSS and Shadcn/ui, mastering responsive design, theme management, and consistent styling with layout components. Learn the fundamentals of backend development, including the distinctions between server and client components, how to fetch data, manage loading states, and implement error handling along with nested layouts. 4 | 5 | Enhance your app with CRUD functionalities through Server Actions, improve user interaction, and ensure data integrity with the Zod library. You will also integrate a database using Supabase, handle image uploads, and implement crucial functionalities like authentication with CLERK Service. 6 | 7 | Conclude the course with the skills to deploy your NextJS app on Vercel, and incorporate features such as prompt handling, response management, and image generation. 8 | 9 | This course offers a practical approach, including numerous challenges to apply what you've learned. Transform your web development skills and gain the confidence to create sophisticated web applications. 10 | --------------------------------------------------------------------------------