87 | >(({ className, ...props }, ref) => (
88 | [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
102 | >(({ className, ...props }, ref) => (
103 |
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 {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
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 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title} }
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | )
31 | })}
32 |
33 |
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 |
--------------------------------------------------------------------------------
/02-home-away-project/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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;
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(
18 | schema: ZodSchema,
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(() => {
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 |
18 |
19 | {children}
20 |
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/03-starter/app/page.tsx:
--------------------------------------------------------------------------------
1 | const HomePage = () => {
2 | return HomeAway Project - Starter ;
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) => )
13 | Breadcrumb.displayName = "Breadcrumb"
14 |
15 | const BreadcrumbList = React.forwardRef<
16 | HTMLOListElement,
17 | React.ComponentPropsWithoutRef<"ol">
18 | >(({ className, ...props }, ref) => (
19 |
27 | ))
28 | BreadcrumbList.displayName = "BreadcrumbList"
29 |
30 | const BreadcrumbItem = React.forwardRef<
31 | HTMLLIElement,
32 | React.ComponentPropsWithoutRef<"li">
33 | >(({ className, ...props }, ref) => (
34 |
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 |
56 | )
57 | })
58 | BreadcrumbLink.displayName = "BreadcrumbLink"
59 |
60 | const BreadcrumbPage = React.forwardRef<
61 | HTMLSpanElement,
62 | React.ComponentPropsWithoutRef<"span">
63 | >(({ className, ...props }, ref) => (
64 |
72 | ))
73 | BreadcrumbPage.displayName = "BreadcrumbPage"
74 |
75 | const BreadcrumbSeparator = ({
76 | children,
77 | className,
78 | ...props
79 | }: React.ComponentProps<"li">) => (
80 | svg]:size-3.5", className)}
84 | {...props}
85 | >
86 | {children ?? }
87 |
88 | )
89 | BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
90 |
91 | const BreadcrumbEllipsis = ({
92 | className,
93 | ...props
94 | }: React.ComponentProps<"span">) => (
95 |
101 |
102 | More
103 |
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,
39 | VariantProps {
40 | asChild?: boolean
41 | }
42 |
43 | const Button = React.forwardRef(
44 | ({ className, variant, size, asChild = false, ...props }, ref) => {
45 | const Comp = asChild ? Slot : "button"
46 | return (
47 |
52 | )
53 | }
54 | )
55 | Button.displayName = "Button"
56 |
57 | export { Button, buttonVariants }
58 |
--------------------------------------------------------------------------------
/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
11 |
12 | function Calendar({
13 | className,
14 | classNames,
15 | showOutsideDays = true,
16 | ...props
17 | }: CalendarProps) {
18 | return (
19 | .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 }) => ,
64 | IconRight: ({ ...props }) => ,
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
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
41 | ))
42 | CardTitle.displayName = "CardTitle"
43 |
44 | const CardDescription = React.forwardRef<
45 | HTMLParagraphElement,
46 | React.HTMLAttributes
47 | >(({ className, ...props }, ref) => (
48 |
53 | ))
54 | CardDescription.displayName = "CardDescription"
55 |
56 | const CardContent = React.forwardRef<
57 | HTMLDivElement,
58 | React.HTMLAttributes
59 | >(({ className, ...props }, ref) => (
60 |
61 | ))
62 | CardContent.displayName = "CardContent"
63 |
64 | const CardFooter = React.forwardRef<
65 | HTMLDivElement,
66 | React.HTMLAttributes
67 | >(({ className, ...props }, ref) => (
68 |
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,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
21 |
24 |
25 |
26 |
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 {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
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,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
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,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
18 |
19 |
29 |
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,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
17 |
18 | {children}
19 |
20 |
21 |
22 |
23 | ))
24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
25 |
26 | const ScrollBar = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, orientation = "vertical", ...props }, ref) => (
30 |
43 |
44 |
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,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = "horizontal", decorative = true, ...props },
14 | ref
15 | ) => (
16 |
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) {
7 | return (
8 |
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
8 | >(({ className, ...props }, ref) => (
9 |
16 | ))
17 | Table.displayName = "Table"
18 |
19 | const TableHeader = React.forwardRef<
20 | HTMLTableSectionElement,
21 | React.HTMLAttributes
22 | >(({ className, ...props }, ref) => (
23 |
24 | ))
25 | TableHeader.displayName = "TableHeader"
26 |
27 | const TableBody = React.forwardRef<
28 | HTMLTableSectionElement,
29 | React.HTMLAttributes
30 | >(({ className, ...props }, ref) => (
31 |
36 | ))
37 | TableBody.displayName = "TableBody"
38 |
39 | const TableFooter = React.forwardRef<
40 | HTMLTableSectionElement,
41 | React.HTMLAttributes
42 | >(({ className, ...props }, ref) => (
43 | 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
57 | >(({ className, ...props }, ref) => (
58 |
66 | ))
67 | TableRow.displayName = "TableRow"
68 |
69 | const TableHead = React.forwardRef<
70 | HTMLTableCellElement,
71 | React.ThHTMLAttributes
72 | >(({ className, ...props }, ref) => (
73 | [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
87 | >(({ className, ...props }, ref) => (
88 | [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
102 | >(({ className, ...props }, ref) => (
103 |
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 {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
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 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title} }
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | )
31 | })}
32 |
33 |
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 |
--------------------------------------------------------------------------------
/03-starter/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------