├── .eslintrc.json ├── utils ├── index.ts ├── guestUtils.ts └── datesutils.ts ├── public ├── favicon.ico └── vercel.svg ├── assets └── airbnb_logo.png ├── postcss.config.js ├── next.config.js ├── hooks └── useDataContext.tsx ├── tailwind.config.js ├── pages ├── api │ └── hello.ts ├── _app.tsx └── index.tsx ├── components ├── Navbar.tsx ├── AppSearchOptionWrapper.tsx ├── interfaces │ └── interfaces.tsx ├── AppClearButton.tsx ├── AppHeaderOption.tsx ├── AppDateRange.tsx ├── AppCounter.tsx ├── AppSearchOptionButton.tsx ├── Applogo.tsx ├── AppHeader.tsx └── AppSearchBar.tsx ├── CONTRIBUTING.md ├── .gitignore ├── styles ├── globals.css └── Home.module.css ├── tsconfig.json ├── context ├── actionTypes.ts ├── store.tsx └── reducer.ts ├── package.json ├── LICENSE.md ├── README.md └── CODE_OF_CONDUCT.md /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './datesutils'; 2 | export * from './guestUtils'; 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aryamankha/airbnb-clone/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /assets/airbnb_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aryamankha/airbnb-clone/HEAD/assets/airbnb_logo.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | } 6 | 7 | module.exports = nextConfig 8 | -------------------------------------------------------------------------------- /hooks/useDataContext.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { DataContext } from '../context/store'; 3 | 4 | export const useDataContext = () => useContext(DataContext); 5 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./pages/**/*.{js,ts,jsx,tsx}", 5 | "./components/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | }; 12 | -------------------------------------------------------------------------------- /pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | res.status(200).json({ name: 'John Doe' }) 13 | } 14 | -------------------------------------------------------------------------------- /components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import React,{FC,useState} from "react"; 2 | import {SearchBarProps} from './interfaces/interfaces'; 3 | 4 | 5 | const Navbar : FC =({ 6 | menu, 7 | isActiveHeader, 8 | closeSearch, 9 | searchPage 10 | })=>{ 11 | 12 | 13 | 14 | return ( 15 | <> 16 |
17 | sadasdads 18 | 19 |
20 | ); 21 | } 22 | 23 | export {Navbar}; 24 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | - Sign up via the hacktoberfest account as a contributor at https://hacktoberfest.com/ 3 | 4 | # Instructions 5 | - Find an open issue 6 | - Request to be assigned to that issue 7 | - Fork this repository 8 | - Clone the forked repository 9 | - Create a new branch 10 | - Make changes to the code 11 | - Commit the changes 12 | - Push the changes to the forked repository 13 | - Create a pull request (with a screenshot if it's a frontend task) 14 | - Wait for your PR to get reviewed 15 | -------------------------------------------------------------------------------- /components/AppSearchOptionWrapper.tsx: -------------------------------------------------------------------------------- 1 | import React,{FC , PropsWithChildren} from 'react'; 2 | 3 | interface AppSearchOptionsProps extends PropsWithChildren{ 4 | className:string 5 | } 6 | const AppSearchOptionWrapper : FC =({className,children})=>{ 7 | return ( 8 |
9 | {children} 10 |
11 | ); 12 | } 13 | export default AppSearchOptionWrapper; -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | import type { AppProps } from "next/app"; 3 | import React from 'react'; 4 | 5 | if ( 6 | typeof window !== "undefined" && 7 | process.env.NODE_ENV === "development" 8 | // && /VIVID_ENABLED=true/.test(document.cookie) 9 | ) { 10 | import("vivid-studio").then((v) => v.run()); 11 | import("vivid-studio/style.css"); 12 | } 13 | 14 | function MyApp({ Component, pageProps }: AppProps) { 15 | return ( 16 | 17 | ); 18 | } 19 | 20 | export default MyApp; 21 | -------------------------------------------------------------------------------- /.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 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.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 | 38 | package-lock.json 39 | yarn.lock 40 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html, 6 | body { 7 | padding: 0; 8 | margin: 0; 9 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 10 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 11 | } 12 | 13 | a { 14 | color: inherit; 15 | text-decoration: none; 16 | } 17 | 18 | * { 19 | box-sizing: border-box; 20 | } 21 | 22 | @media (prefers-color-scheme: dark) { 23 | html { 24 | color-scheme: dark; 25 | } 26 | body { 27 | color: white; 28 | background: black; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /utils/guestUtils.ts: -------------------------------------------------------------------------------- 1 | import { FormatGuestOptions } from "../components/interfaces/interfaces"; 2 | 3 | export const formatGuests = (guests:any,options?:FormatGuestOptions)=>{ 4 | if(!guests) return false; 5 | const {noInfants} = options|| {}; 6 | const {children,adults,infants} = guests; 7 | const total = adults+children; 8 | if(!total){return 0;} 9 | let template = `${total} guest`; 10 | if(total >=2){ 11 | template = `${total} guests`; 12 | } 13 | if(infants&& !noInfants){ 14 | template += ` , ${infants} infant`; 15 | } 16 | return template; 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "components/Navbar.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /context/actionTypes.ts: -------------------------------------------------------------------------------- 1 | export enum DATA_ACTION_TYPES { 2 | SET_LOCATION = "SET_LOCATION", 3 | SET_CHECK_IN = "SET_CHECK_IN", 4 | SET_CHECK_OUT = "SET_CHECK_OUT", 5 | SET_GUESTS = "SET_GUESTS", 6 | RESET_DATES = "RESET_DATES", 7 | RESET_GUESTS = "RESET_GUESTS", 8 | INCREASE_ADULTS = "INCREASE_ADULTS", 9 | INCREASE_CHILDREN = "INCREASE_CHILDREN", 10 | INCREASE_INFANTS = "INCREASE_INFANTS", 11 | DECREASE_ADULTS = "DECREASE_ADULTS", 12 | DECREASE_CHILDREN = "DECREASE_CHILDREN", 13 | DECREASE_INFANTS = "DECREASE_INFANTS", 14 | } 15 | 16 | // Implement this 17 | export type IDataActionPayload = any; 18 | 19 | export interface IDataAction { 20 | type: DATA_ACTION_TYPES; 21 | payload: IDataActionPayload; 22 | } 23 | -------------------------------------------------------------------------------- /utils/datesutils.ts: -------------------------------------------------------------------------------- 1 | import { format } from "date-fns"; 2 | export const formatCheckDate=(date:Date,dateFormat?:string)=>{ 3 | if(!date){return '';} 4 | return format(date,dateFormat || 'MMM d' ); 5 | }; 6 | 7 | export const formatRangeDate = (startDate :Date,endDate :Date)=>{ 8 | if(!startDate || !endDate){return false;} 9 | let template = `${formatCheckDate(startDate)} - ${formatCheckDate(endDate)}`; 10 | if(formatCheckDate(startDate,'d m y') === formatCheckDate(endDate,'d m y') ){ 11 | template = `${formatCheckDate(startDate)} - ${parseInt(formatCheckDate(endDate,'d')) +1 }`; 12 | } 13 | if (formatCheckDate(startDate, 'y') !== formatCheckDate(endDate, 'y')) { 14 | template = `${formatCheckDate(startDate, 'MMM d, y')} - ${formatCheckDate( 15 | endDate, 16 | 'MMM d, y' 17 | )}`; 18 | } 19 | return template; 20 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-tailwind-vivid-starter", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev -p 3005", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@heroicons/react": "^1.0.6", 13 | "date-fns": "^2.29.3", 14 | "next": "12.3.1", 15 | "react": "18.2.0", 16 | "react-date-range": "^1.4.0", 17 | "react-dom": "18.2.0" 18 | }, 19 | "devDependencies": { 20 | "@types/node": "18.8.4", 21 | "@types/react": "18.0.21", 22 | "@types/react-date-range": "^1.4.4", 23 | "@types/react-dom": "18.0.6", 24 | "autoprefixer": "^10.4.12", 25 | "eslint": "8.25.0", 26 | "eslint-config-next": "12.3.1", 27 | "postcss": "^8.4.17", 28 | "tailwindcss": "^3.1.8", 29 | "typescript": "4.8.4", 30 | "vivid-studio": "^0.14.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next' 2 | import { useState } from 'react' 3 | import AppHeader from '../components/AppHeader'; 4 | import Head from 'next/head'; 5 | import Image from 'next/image' 6 | 7 | const Home: NextPage = () => { 8 | const [location, setLocation] = useState(''); 9 | const [checkIn, setCheckIn] = useState(); 10 | const [checkOut, setCheckOut] = useState(); 11 | const [guests, setGuests] = useState(); 12 | 13 | return ( 14 | // Base Container 15 |
16 | {/* Title */} 17 | 18 | airbnb 19 | 20 | {/* Logo */} 21 |
22 | Logo 23 |
24 | 25 | 26 | 27 | {/* Horizontal Rule */} 28 |
29 |
30 | ); 31 | } 32 | 33 | export default Home 34 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /components/interfaces/interfaces.tsx: -------------------------------------------------------------------------------- 1 | interface exploreNearby { 2 | location: string; 3 | img: string; 4 | distance: string; 5 | } 6 | 7 | interface HeaderProps { 8 | exploreNearby?: exploreNearby[]; 9 | searchPage?: boolean; 10 | query?: any; 11 | } 12 | 13 | 14 | export enum HeaderOptions { 15 | PLACES_TO_STAY = 'placesToStay', 16 | FIND_EXPERIENCES = 'findExperiences', 17 | }; 18 | 19 | export enum EAppLogo { 20 | LOGO = 'logo', 21 | TEXT = 'text', 22 | }; 23 | export enum SearchMenu{ 24 | LOCATION= 'location', 25 | CHECK_IN = 'checkIn', 26 | CHECK_OUT = 'checkOut', 27 | GUESTS = 'guests', 28 | }; 29 | 30 | interface SearchBarProps{ 31 | menu: HeaderOptions | null; 32 | isActiveHeader: boolean; 33 | searchPage?: boolean; 34 | closeSearch?: () => void; 35 | }; 36 | interface LogoProps { 37 | className?:string, 38 | type?:EAppLogo, 39 | }; 40 | 41 | 42 | interface FormatGuestOptions{ 43 | noInfants?:boolean 44 | } 45 | 46 | export type { HeaderProps, exploreNearby,SearchBarProps,LogoProps, FormatGuestOptions}; 47 | -------------------------------------------------------------------------------- /components/AppClearButton.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | // icons 3 | import { XIcon } from '@heroicons/react/outline'; 4 | 5 | interface AppClearButtonProps { 6 | active: boolean; 7 | isFocus?: boolean; 8 | separator?: boolean; 9 | onClick: () => void; 10 | } 11 | 12 | const defaultProps = { 13 | isFocus: true, 14 | separator: false, 15 | }; 16 | 17 | const AppClearButtonProps: FC = ({ 18 | onClick, 19 | active, 20 | isFocus, 21 | separator, 22 | }) => { 23 | return ( 24 |
25 |
33 | 34 |
35 |
36 | ); 37 | }; 38 | 39 | AppClearButtonProps.defaultProps = defaultProps; 40 | 41 | export default AppClearButtonProps; 42 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 SupremelySAM 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /components/AppHeaderOption.tsx: -------------------------------------------------------------------------------- 1 | import { FC, PropsWithChildren } from 'react'; 2 | 3 | interface IAppHeaderOptionProps extends PropsWithChildren { 4 | active?: boolean; 5 | isSnap?: boolean; 6 | isActiveHeader?: boolean; 7 | onClick?: () => void; 8 | } 9 | 10 | const AppHeaderOption: FC = ({ 11 | children, 12 | active, 13 | isSnap, 14 | isActiveHeader, 15 | onClick, 16 | }) => { 17 | const headerMenuBehavior = () => { 18 | let style = []; 19 | if (isSnap) style.push('text-white after:bg-white'); 20 | if (!isSnap) style.push('text-gray-500 after:bg-gray-500'); 21 | if (isActiveHeader) style.push('inline-block'); 22 | if (!isActiveHeader) style.push('hidden'); 23 | return style.join(' '); 24 | }; 25 | 26 | return ( 27 | 33 | {children} 34 | 35 | ); 36 | }; 37 | 38 | export default AppHeaderOption; 39 | -------------------------------------------------------------------------------- /components/AppDateRange.tsx: -------------------------------------------------------------------------------- 1 | import {FC} from 'react' 2 | import {DateRange} from 'react-date-range'; 3 | // context 4 | import { DATA_ACTION_TYPES } from './../context/actionTypes'; 5 | import { useDataContext } from './../hooks/useDataContext'; 6 | 7 | interface AppDateRangeProps{ 8 | months?:number, 9 | } 10 | const AppDateRange :FC = ({months})=>{ 11 | const [{ checkIn, checkOut }, dispatch] = useDataContext(); 12 | 13 | const selectionRange = { 14 | startDate: checkIn, 15 | endDate: checkOut, 16 | key: 'selection', 17 | }; 18 | 19 | const handleDatePicker = (range) => { 20 | const { startDate, endDate } = range.selection; 21 | dispatch({ type: DATA_ACTION_TYPES.SET_CHECK_IN, payload: startDate }); 22 | dispatch({ type: DATA_ACTION_TYPES.SET_CHECK_OUT, payload: endDate }); 23 | }; 24 | return ( 25 |
26 | 37 |
38 | ); 39 | }; 40 | export default AppDateRange; -------------------------------------------------------------------------------- /components/AppCounter.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | // icons 3 | import { MinusIcon, PlusIcon } from '@heroicons/react/outline'; 4 | 5 | interface IAppCounterProps { 6 | value: number; 7 | maxValue: number; 8 | onIncrease: () => void; 9 | onDescrease: () => void; 10 | } 11 | 12 | const AppCounter: FC = ({ 13 | value, 14 | maxValue, 15 | onIncrease, 16 | onDescrease, 17 | }) => { 18 | return ( 19 |
20 | 28 | 29 | 30 | {value} 31 | 39 | 40 | 41 |
42 | ); 43 | }; 44 | 45 | export default AppCounter; 46 | -------------------------------------------------------------------------------- /context/store.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useReducer, type Dispatch } from "react"; 2 | import { dataReducer } from "./reducer"; 3 | import { IDataAction } from "./actionTypes"; 4 | 5 | interface IInitialState { 6 | location: string; 7 | checkIn: Date | null; 8 | checkOut: Date | null; 9 | guests: { 10 | adults: number; 11 | children: number; 12 | infants: number; 13 | }; 14 | } 15 | 16 | /** 17 | * TODO add documentation all types 18 | * with tsdoc! 19 | */ 20 | export interface IDataContext { 21 | location: string; 22 | checkIn: Date | null; 23 | checkOut: Date | null; 24 | guests: { 25 | adults: number; 26 | children: number; 27 | infants: number; 28 | }; 29 | } 30 | 31 | export const initialState: IDataContext = { 32 | location: "", 33 | checkIn: null, 34 | checkOut: null, 35 | guests: { adults: 0, children: 0, infants: 0 }, 36 | }; 37 | 38 | // export const DataContext = createContext(initialState); 39 | export const DataContext = createContext<[IDataContext, Dispatch]>( 40 | [ 41 | initialState, 42 | () => { 43 | return initialState; 44 | }, 45 | ] 46 | ); 47 | 48 | export type Props = { 49 | children: React.ReactNode; 50 | }; 51 | 52 | export const ContextProvider = ({ children }: Props) => { 53 | const [state, dispatch] = useReducer(dataReducer, initialState); 54 | 55 | return ( 56 | 67 | {children} 68 | 69 | ); 70 | }; 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Airbnb Clone 2 | 3 | Building an Airbnb clone with Vivid - the in-browser styling editor that makes Tailwind CSS even faster. 4 | 5 |

6 |

7 | Hacktoberfest 2022! 8 |

9 |

10 | 11 | ![image](https://user-images.githubusercontent.com/75615789/192692200-a4155b0d-bdc3-4a05-9747-ea7d2d6f665b.png) 12 | 13 | ## Getting Started 14 | 15 | Clone the repository, install the necessary dependencies, and then run the application. 16 | 17 | ```bash 18 | git clone https://github.com/aryamankha/airbnb-clone.git 19 | npm install 20 | npm run dev 21 | # or 22 | git clone https://github.com/aryamankha/airbnb-clone.git 23 | yarn 24 | yarn dev 25 | ``` 26 | 27 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. Go over the Vivid docs at [docs.vivid.lol](https://docs.vivid.lol) to get familiar with how in-browser styling works. 28 | 29 | ## Contributing 30 | 31 | This repository is beginner friendly! We'll be creating issues for contributors to own components in the frontend, but feel free to propose your own issues! The goal is to make our frontend replicate the [Airbnb site] (https://www.airbnb.com) as closely as possible. We might not actually be building out a backend for this project, but be sure to structure your frontend code in a way that could later be wired to a database. For example, if creating a component to represent an individual listing, don't hardcode every single text field - those should be passed in through props. 32 | 33 | ## Code organization 34 | 35 | We'll be using React, Next, and Typescript for our frontend with Tailwind for all styling. Any new sections created should be added to the "components" folder, with components put together to create full pages. 36 | 37 | ## What is Vivid? 38 | 39 | Vivid is an in-browser styling we've developed (check out www.style.vivid.lol). It's still in alpha, but it allows you to style with Tailwind directly from your browser. Check out our [docs](https://docs.vivid.lol) to learn how it works! If you find any bugs in Vivid, feel free to create issues in this repository so we can address them. Would love your feedback! 40 | 41 | ![Pitch Demo](https://user-images.githubusercontent.com/62365335/195212355-23c3f95f-4eb0-4ee0-ad27-481284eb946c.gif) 42 | 43 | ## Designs 44 | 45 | We haven't created Figma designs for this project, but the live Airbnb site is the best reference point! 46 | -------------------------------------------------------------------------------- /styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0 2rem; 3 | } 4 | 5 | .main { 6 | min-height: 100vh; 7 | padding: 4rem 0; 8 | flex: 1; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | align-items: center; 13 | } 14 | 15 | .footer { 16 | display: flex; 17 | flex: 1; 18 | padding: 2rem 0; 19 | border-top: 1px solid #eaeaea; 20 | justify-content: center; 21 | align-items: center; 22 | } 23 | 24 | .footer a { 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | flex-grow: 1; 29 | } 30 | 31 | .title a { 32 | color: #0070f3; 33 | text-decoration: none; 34 | } 35 | 36 | .title a:hover, 37 | .title a:focus, 38 | .title a:active { 39 | text-decoration: underline; 40 | } 41 | 42 | .title { 43 | margin: 0; 44 | line-height: 1.15; 45 | font-size: 4rem; 46 | } 47 | 48 | .title, 49 | .description { 50 | text-align: center; 51 | } 52 | 53 | .description { 54 | margin: 4rem 0; 55 | line-height: 1.5; 56 | font-size: 1.5rem; 57 | } 58 | 59 | .code { 60 | background: #fafafa; 61 | border-radius: 5px; 62 | padding: 0.75rem; 63 | font-size: 1.1rem; 64 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 65 | Bitstream Vera Sans Mono, Courier New, monospace; 66 | } 67 | 68 | .grid { 69 | display: flex; 70 | align-items: center; 71 | justify-content: center; 72 | flex-wrap: wrap; 73 | max-width: 800px; 74 | } 75 | 76 | .card { 77 | margin: 1rem; 78 | padding: 1.5rem; 79 | text-align: left; 80 | color: inherit; 81 | text-decoration: none; 82 | border: 1px solid #eaeaea; 83 | border-radius: 10px; 84 | transition: color 0.15s ease, border-color 0.15s ease; 85 | max-width: 300px; 86 | } 87 | 88 | .card:hover, 89 | .card:focus, 90 | .card:active { 91 | color: #0070f3; 92 | border-color: #0070f3; 93 | } 94 | 95 | .card h2 { 96 | margin: 0 0 1rem 0; 97 | font-size: 1.5rem; 98 | } 99 | 100 | .card p { 101 | margin: 0; 102 | font-size: 1.25rem; 103 | line-height: 1.5; 104 | } 105 | 106 | .logo { 107 | height: 1em; 108 | margin-left: 0.5rem; 109 | } 110 | 111 | @media (max-width: 600px) { 112 | .grid { 113 | width: 100%; 114 | flex-direction: column; 115 | } 116 | } 117 | 118 | @media (prefers-color-scheme: dark) { 119 | .card, 120 | .footer { 121 | border-color: #222; 122 | } 123 | .code { 124 | background: #111; 125 | } 126 | .logo img { 127 | filter: invert(1); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /context/reducer.ts: -------------------------------------------------------------------------------- 1 | import { DATA_ACTION_TYPES, IDataAction } from "./actionTypes"; 2 | import { IDataContext, initialState } from "./store"; 3 | 4 | export const dataReducer = (state: IDataContext, action: IDataAction) => { 5 | const { type, payload } = action; 6 | const { adults, children, infants } = state.guests; 7 | switch (type) { 8 | case DATA_ACTION_TYPES.SET_LOCATION: 9 | return { ...state, location: payload }; 10 | 11 | case DATA_ACTION_TYPES.SET_CHECK_IN: 12 | return { ...state, checkIn: payload }; 13 | 14 | case DATA_ACTION_TYPES.SET_CHECK_OUT: 15 | return { ...state, checkOut: payload }; 16 | 17 | case DATA_ACTION_TYPES.SET_GUESTS: 18 | return { ...state, guests: payload }; 19 | 20 | case DATA_ACTION_TYPES.RESET_DATES: 21 | return { ...state, checkOut: null, checkIn: null }; 22 | 23 | case DATA_ACTION_TYPES.RESET_GUESTS: 24 | return { ...state, guests: initialState.guests }; 25 | 26 | case DATA_ACTION_TYPES.INCREASE_ADULTS: 27 | if (adults >= 16) return state; 28 | return { ...state, guests: { ...state.guests, adults: adults + 1 } }; 29 | 30 | case DATA_ACTION_TYPES.INCREASE_CHILDREN: 31 | if (children >= 5) return state; 32 | if (adults <= 0) { 33 | return { 34 | ...state, 35 | guests: { 36 | ...state.guests, 37 | children: children + 1, 38 | adults: adults + 1, 39 | }, 40 | }; 41 | } 42 | return { ...state, guests: { ...state.guests, children: children + 1 } }; 43 | 44 | case DATA_ACTION_TYPES.INCREASE_INFANTS: 45 | if (infants >= 5) return state; 46 | if (adults <= 0) { 47 | return { 48 | ...state, 49 | guests: { ...state.guests, infants: infants + 1, adults: adults + 1 }, 50 | }; 51 | } 52 | return { ...state, guests: { ...state.guests, infants: infants + 1 } }; 53 | 54 | case DATA_ACTION_TYPES.DECREASE_ADULTS: 55 | if (adults <= 0) return state; 56 | if (adults <= 1 && (children >= 1 || infants >= 1)) return state; 57 | return { ...state, guests: { ...state.guests, adults: adults - 1 } }; 58 | 59 | case DATA_ACTION_TYPES.DECREASE_CHILDREN: 60 | if (children <= 0) return state; 61 | return { ...state, guests: { ...state.guests, children: children - 1 } }; 62 | 63 | case DATA_ACTION_TYPES.DECREASE_INFANTS: 64 | if (infants <= 0) return state; 65 | return { ...state, guests: { ...state.guests, infants: infants - 1 } }; 66 | default: 67 | return state; 68 | } 69 | }; 70 | -------------------------------------------------------------------------------- /components/AppSearchOptionButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { ChangeEvent, FC, FocusEvent, PropsWithChildren } from 'react'; 2 | // components 3 | import AppClearButtonProps from './AppClearButton'; 4 | // icons 5 | import { SearchIcon } from '@heroicons/react/outline'; 6 | 7 | interface IAppSearchOptionButtonProps extends PropsWithChildren { 8 | relative?: boolean; 9 | withSearch?: boolean; 10 | separator?: boolean; 11 | isSearch?: boolean; 12 | type?: string; 13 | title: string; 14 | placeholder: string; 15 | active: boolean; 16 | value: any; 17 | onChange?: (event: ChangeEvent) => void; 18 | onFocus: () => void; 19 | onBlur: (event: FocusEvent) => void; 20 | onClear: () => void; 21 | } 22 | 23 | const AppSearchOptionButton: FC = ({ 24 | relative, 25 | children, 26 | separator, 27 | withSearch, 28 | isSearch, 29 | type, 30 | title, 31 | placeholder, 32 | active, 33 | value, 34 | onChange, 35 | onFocus, 36 | onBlur, 37 | onClear, 38 | }) => { 39 | return ( 40 | 49 |
54 | {title} 55 | {type === 'inputText' ? ( 56 | 64 | ) : ( 65 | 66 | {value || placeholder} 67 | 68 | )} 69 |
70 | 71 | {/* clear icon */} 72 | 78 | 79 | {withSearch && ( 80 | 95 | )} 96 |
{children}
97 |
98 | ); 99 | }; 100 | 101 | export default AppSearchOptionButton; 102 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [aryamankha](https://github.com/aryamankha). The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /components/Applogo.tsx: -------------------------------------------------------------------------------- 1 | import React,{useState,useEffect,FC} from 'react'; 2 | import { LogoProps,EAppLogo } from './interfaces/interfaces'; 3 | 4 | const defaultProps:LogoProps={ 5 | className:'text-black', 6 | type:EAppLogo.TEXT, 7 | } 8 | const AppLogo: FC = ({ className, type }) => { 9 | switch (type) { 10 | case EAppLogo.LOGO: 11 | return ( 12 | 18 | 19 | 20 | ); 21 | case EAppLogo.TEXT: 22 | return ( 23 | 29 | 30 | 31 | ); 32 | default: 33 | return <>Error occured I guess; 34 | } 35 | }; 36 | 37 | AppLogo.defaultProps = defaultProps; 38 | export default AppLogo; -------------------------------------------------------------------------------- /components/AppHeader.tsx: -------------------------------------------------------------------------------- 1 | import React,{useState, FC, useEffect} from 'react'; 2 | import {exploreNearby,HeaderProps,HeaderOptions, SearchMenu,EAppLogo} from './interfaces/interfaces'; 3 | import { formatGuests } from '../utils/guestUtils'; 4 | import {Navbar} from './Navbar'; 5 | import AppLogo from './Applogo'; 6 | import Link from 'next/link'; 7 | import { formatRangeDate } from '../utils/datesutils'; 8 | import AppHeaderOption from './AppHeaderOption'; 9 | import AppSearchBar from './AppSearchBar'; 10 | import {GlobeAltIcon,MenuIcon,SearchIcon,UserCircleIcon} from '@heroicons/react/outline'; 11 | const AppHeader: FC = ({ exploreNearby, searchPage, query }) => { 12 | const [isSnapTop, setIsSnapTop] = useState(searchPage ? false : true); 13 | const [isActiveSearch, setIsActiveSearch] = useState( 14 | searchPage ? false : true 15 | ); 16 | const [activeMenu, setActiveMenu] = useState( 17 | HeaderOptions.PLACES_TO_STAY 18 | ); 19 | 20 | const handleOnScroll = () => { 21 | const position = window.scrollY; 22 | if (position >= 50) { 23 | setIsSnapTop(false); 24 | setIsActiveSearch(false); 25 | } else { 26 | setIsSnapTop(true); 27 | setIsActiveSearch(true); 28 | } 29 | }; 30 | 31 | const headerBehaviour = () => { 32 | let style = []; 33 | if (!isSnapTop) style.push('bg-white shadow-lg'); 34 | if (!isActiveSearch) style.push('h-[86px] pb-5'); 35 | if (isActiveSearch) style.push('pb-8'); 36 | return style.join(' '); 37 | }; 38 | 39 | useEffect(() => { 40 | // listen to scroll 41 | if (!searchPage) { 42 | window.addEventListener('scroll', handleOnScroll); 43 | } 44 | return () => window.removeEventListener('scroll', handleOnScroll); 45 | }, [searchPage]); 46 | 47 | return( 48 | <> 49 |
50 | 154 | {/* main search bar */} 155 | setIsActiveSearch(false)} 160 | /> 161 | {/* mobile search bar */} 162 | {/* Left to be done */} 163 |
164 | {/* background layer */} 165 | {isActiveSearch && !isSnapTop && ( 166 |
setIsActiveSearch(false)} 169 | /> 170 | )} 171 | ); 172 | }; 173 | export default AppHeader; -------------------------------------------------------------------------------- /components/AppSearchBar.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, FocusEvent, FormEvent, useContext, useState } from "react"; 2 | import { useRouter } from "next/router"; 3 | // components 4 | import AppSearchOptionButton from "./AppSearchOptionButton"; 5 | import AppCounter from "./AppCounter"; 6 | import AppDateRange from "./AppDateRange"; 7 | import AppSearchOptionWrapper from "./AppSearchOptionWrapper"; 8 | import { useDataContext } from "./../hooks/useDataContext"; 9 | import { DATA_ACTION_TYPES } from "./../context/actionTypes"; 10 | // styles 11 | import "react-date-range/dist/styles.css"; 12 | import "react-date-range/dist/theme/default.css"; 13 | // icons 14 | import { ChevronRightIcon } from "@heroicons/react/outline"; 15 | import { HeaderOptions } from "./interfaces/interfaces"; 16 | // utils 17 | import { formatCheckDate, formatRangeDate, formatGuests } from "./../utils"; 18 | // import { DataContext } from '../context/store'; 19 | 20 | enum ESearchMenu { 21 | LOCATION = "location", 22 | CHECK_IN = "checkIn", 23 | CHECK_OUT = "checkOut", 24 | GUESTS = "guests", 25 | } 26 | 27 | interface IAppSearchBarProps { 28 | menu: HeaderOptions | null; 29 | isActiveHeader: boolean; 30 | searchPage?: boolean; 31 | closeSearch: () => void; 32 | } 33 | 34 | const AppSearchBar: FC = ({ 35 | menu, 36 | isActiveHeader, 37 | closeSearch, 38 | searchPage, 39 | }) => { 40 | const router = useRouter(); 41 | const [searchMenu, setSearchMenu] = useState(null); 42 | // data 43 | const [{ location, checkIn, checkOut, guests }, dispatch] = useDataContext(); 44 | // handler 45 | const handleOnBlur = (event?: FocusEvent) => { 46 | const { relatedTarget } = event || {}; 47 | if (!relatedTarget) { 48 | setSearchMenu(null); 49 | return; 50 | } 51 | const relatedTargetClassList = Array.from( 52 | (relatedTarget as Element)?.classList 53 | ); 54 | const result = relatedTargetClassList.some((className) => { 55 | const prefix = ["rdr", "btn"]; 56 | if (prefix.includes(className.slice(0, 3))) return true; 57 | }); 58 | if (!result) setSearchMenu(null); 59 | }; 60 | 61 | const resetDate = () => { 62 | dispatch({ 63 | type: DATA_ACTION_TYPES.RESET_DATES, 64 | payload: undefined, 65 | }); 66 | handleOnBlur(); 67 | }; 68 | 69 | const handleOnSubmit = (event: FormEvent) => { 70 | event.preventDefault(); 71 | if (!location) { 72 | setSearchMenu(ESearchMenu.LOCATION); 73 | return; 74 | } 75 | if (searchPage) closeSearch(); 76 | setSearchMenu(null); 77 | 78 | router.push({ 79 | pathname: "/search", 80 | query: { 81 | location, 82 | checkIn: checkIn?.toISOString(), 83 | checkOut: checkOut?.toISOString(), 84 | guests: JSON.stringify(guests), 85 | }, 86 | }); 87 | }; 88 | 89 | const dateRangeStyle = 90 | "left-4 right-4 searchbar:left-auto searchbar:right-1/2 searchbar:translate-x-1/2 searchbar:w-[850px]"; 91 | 92 | return ( 93 | <> 94 |
95 | 292 |
293 | 294 | ); 295 | }; 296 | 297 | export default AppSearchBar; 298 | --------------------------------------------------------------------------------