Skip to content

Commit

Permalink
Merge pull request #6 from lGnyte/accommodations
Browse files Browse the repository at this point in the history
[Web-012] Begin adding accommodations to webapp
  • Loading branch information
lGnyte authored May 25, 2024
2 parents 0115fec + 1230c6a commit 31307e5
Show file tree
Hide file tree
Showing 9 changed files with 401 additions and 3 deletions.
173 changes: 173 additions & 0 deletions src/app/add_accommodation/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
'use client';

import { ChangeEvent, FormEvent, useState, useContext } from "react";
import { addDoc, collection } from 'firebase/firestore';
import { ref, uploadBytesResumable, getDownloadURL } from 'firebase/storage';
import { db, storage } from '@/lib/firebase';
import toast from "react-hot-toast";
import { UserContext } from '@/lib/context';
import { createHash } from "crypto";
import ConfirmationModal from "@/components/AsyncConfirmationModal";
import HostCheck from "@/components/usertype_check/HostCheck";

export default function AddAccommodation() {
const [location, setLocation] = useState<string>('');
const [title, setTitle] = useState<string>('');
const [description, setDescription] = useState<string>('');
const [photos, setPhotos] = useState<File[]>([]);
const [numberOfRooms, setNumberOfRooms] = useState<number>(1);
const [price, setPrice] = useState<number>(100);
const [showModal, setShowModal] = useState<boolean>(false);

const { user } = useContext(UserContext);

const handlePhotoUpload = (e: ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
setPhotos(Array.from(e.target.files));
}
};

const handleSubmit = async (e: FormEvent) => {
e.preventDefault();

// Generate a unique ID and add the location data to Firestore
try {

let images = [];

for(let i = 0; i < photos.length; i++) {
const buffer = await photos[i].arrayBuffer();
const name = photos[i].name;
const hashName = createHash('sha256')
.update(new Uint8Array(buffer))
.digest('hex');
const extension = name.split('.').pop();
const storageUri = `accommodations/${hashName}.${extension}`;
const storageRef = ref(storage, storageUri);
if (buffer !== undefined) {
await uploadBytesResumable(storageRef, buffer);
images.push(await getDownloadURL(storageRef));
}
}

await addDoc(collection(db, 'accommodations'), {
uid: user.uid,
title,
location,
description,
numberOfRooms,
price,
photos: images, // set the photos url
});

// Reset the form
setTitle('');
setLocation('');
setDescription('');
setPhotos([]);
setNumberOfRooms(1);
setPrice(100);
setShowModal(false);

toast.success("Successfully created accommodation!")
} catch (e) {
toast.error("Could not create accommodation, contact Admin! Error: "+e);
}
};

const handleFormSubmit = async (e: FormEvent) => {
e.preventDefault();
setShowModal(true);
};


return (
<HostCheck>
<div className="min-h-screen bg-gray-100 flex items-center justify-center p-6">
<div className="bg-white p-6 rounded-lg shadow-lg w-full max-w-lg">
<h2 className="text-2xl font-bold mb-4">Add accommodation</h2>
<form onSubmit={handleFormSubmit}>
<div className="mb-4">
<label className="block text-gray-700">Title</label>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="mt-1 p-2 border border-gray-300 rounded w-full"
required
/>
</div>
<div className="mb-4">
<label className="block text-gray-700">
Location
<span className="text-gray-500 text-sm ml-2">
(e.g., Brasov, Brasov, Romania)
</span>
</label>
<input
type="text"
value={location}
onChange={(e) => setLocation(e.target.value)}
className="mt-1 p-2 border border-gray-300 rounded w-full"
required
/>
</div>
<div className="mb-4">
<label className="block text-gray-700">Description</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
className="mt-1 p-2 border border-gray-300 rounded w-full"
rows={4}
required
/>
</div>
<div className="mb-4">
<label className="block text-gray-700">Upload Photos</label>
<input
type="file"
onChange={handlePhotoUpload}
className="mt-1 p-2 border border-gray-300 rounded w-full"
multiple
required
/>
</div>
<div className="mb-4">
<label className="block text-gray-700">Number of Rooms</label>
<input
type="number"
value={numberOfRooms}
onChange={(e) => setNumberOfRooms(Number(e.target.value))}
className="mt-1 p-2 border border-gray-300 rounded w-full"
required
/>
</div>
<div className="mb-4">
<label className="block text-gray-700">Price per Room</label>
<input
type="number"
value={price}
onChange={(e) => setPrice(Number(e.target.value))}
className="mt-1 p-2 border border-gray-300 rounded w-full"
required
/>
</div>
<button
type="submit"
className="bg-blue-500 text-white p-2 rounded w-full"
>
Submit
</button>
</form>
</div>
<ConfirmationModal
show={showModal}
onClose={() => setShowModal(false)}
onConfirm={handleSubmit}
title="Confirm Submission"
description="Are you sure you want to submit this accommodation?"
/>
</div>
</HostCheck>
);
}
2 changes: 1 addition & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import Header from "@/components/Header";
const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
title: "[CC] Turismo",
title: "Trek Trill",
description: "Find accommodation for you and your group of friends!",
};

Expand Down
82 changes: 82 additions & 0 deletions src/app/my_accommodations/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
'use client';
import { useContext, useEffect, useState } from 'react';
import { DocumentData, collection, deleteDoc, doc, getDocs} from 'firebase/firestore';
import { db } from '@/lib/firebase';
import { UserContext } from '@/lib/context';
import toast from 'react-hot-toast';
import ConfirmationModal from '@/components/AsyncConfirmationModal';
import HostCheck from '@/components/usertype_check/HostCheck';

const MyAccommodations: React.FC = () => {
const { user } = useContext(UserContext);
const [accommodations, setAccommodations] = useState([] as (string | DocumentData)[]);
const [showModal, setShowModal] = useState(false);
const [selectedAccommodationId, setSelectedAccommodationId] = useState("");

const handleGetAccommodations = async () => {
const querySnapshot = await getDocs(collection(db, 'accommodations'));
const accommodations = querySnapshot.docs.filter(doc => (doc.get("uid") == user.uid)).map(doc => [doc.id, doc.data()]);
setAccommodations(accommodations);
}

useEffect(() => {
handleGetAccommodations();
}, [user]); // add the user context as dependency

const handleDeleteAccommodation = async (key: string) => {
try {
const accommodationRef = doc(db, 'accommodations', key);
await deleteDoc(accommodationRef);
await handleGetAccommodations();
} catch (e) {
toast.error("Could not delete accommodation! Error: "+e);
}
setShowModal(false);
};

return (
<HostCheck>
<div className="container mx-auto">
<div className="mt-8 mb-4">
<a href="/add_accommodation" className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
Add accommodation
</a>
</div>
<div>
{accommodations.map((value) => (
<div key={value.at(0)} className="flex items-center justify-between border-b border-gray-200 py-4">
<div>
<h2 className="text-lg font-semibold">{value.at(1).title}</h2>
<p className="text-gray-600">{value.at(1).price} lei</p>
</div>
<div className="flex items-center space-x-4">
<a href={`/edit_accommodation/${value.at(0)}`} className="text-blue-500 hover:text-blue-700">
<svg className="h-6 w-6" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 4H7.2C6.0799 4 5.51984 4 5.09202 4.21799C4.71569 4.40974 4.40973 4.7157 4.21799 5.09202C4 5.51985 4 6.0799 4 7.2V16.8C4 17.9201 4 18.4802 4.21799 18.908C4.40973 19.2843 4.71569 19.5903 5.09202 19.782C5.51984 20 6.0799 20 7.2 20H16.8C17.9201 20 18.4802 20 18.908 19.782C19.2843 19.5903 19.5903 19.2843 19.782 18.908C20 18.4802 20 17.9201 20 16.8V12.5M15.5 5.5L18.3284 8.32843M10.7627 10.2373L17.411 3.58902C18.192 2.80797 19.4584 2.80797 20.2394 3.58902C21.0205 4.37007 21.0205 5.6364 20.2394 6.41745L13.3774 13.2794C12.6158 14.0411 12.235 14.4219 11.8012 14.7247C11.4162 14.9936 11.0009 15.2162 10.564 15.3882C10.0717 15.582 9.54378 15.6885 8.48793 15.9016L8 16L8.04745 15.6678C8.21536 14.4925 8.29932 13.9048 8.49029 13.3561C8.65975 12.8692 8.89125 12.4063 9.17906 11.9786C9.50341 11.4966 9.92319 11.0768 10.7627 10.2373Z" stroke="#000000" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</a>
<button className="text-red-500 hover:text-red-700" onClick={() => {setSelectedAccommodationId(value.at(0)); setShowModal(true)}}>
<svg className="h-6 w-6" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" strokeWidth="0"></g><g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g><g id="SVGRepo_iconCarrier">
<path d="M18 6L17.1991 18.0129C17.129 19.065 17.0939 19.5911 16.8667 19.99C16.6666 20.3412 16.3648 20.6235 16.0011 20.7998C15.588 21 15.0607 21 14.0062 21H9.99377C8.93927 21 8.41202 21 7.99889 20.7998C7.63517 20.6235 7.33339 20.3412 7.13332 19.99C6.90607 19.5911 6.871 19.065 6.80086 18.0129L6 6M4 6H20M16 6L15.7294 5.18807C15.4671 4.40125 15.3359 4.00784 15.0927 3.71698C14.8779 3.46013 14.6021 3.26132 14.2905 3.13878C13.9376 3 13.523 3 12.6936 3H11.3064C10.477 3 10.0624 3 9.70951 3.13878C9.39792 3.26132 9.12208 3.46013 8.90729 3.71698C8.66405 4.00784 8.53292 4.40125 8.27064 5.18807L8 6M14 10V17M10 10V17" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
</path>
</g>
</svg>
</button>
</div>
</div>
))}
</div>
<ConfirmationModal
show={showModal}
onClose={() => setShowModal(false)}
onConfirm={async (_e) => await handleDeleteAccommodation(selectedAccommodationId)}
title="Confirm Submission"
description="Are you sure you want to delete this accommodation? Operation cannot be undone!"
/>
</div>
</HostCheck>
);
};

export default MyAccommodations;

44 changes: 44 additions & 0 deletions src/components/AsyncConfirmationModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React, { FormEvent } from 'react';

interface ConfirmationModalProps {
show: boolean;
onClose: () => void;
onConfirm: (e: FormEvent) => Promise<void>;
title: string;
description: string;
}

const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
show,
onClose,
onConfirm,
title,
description,
}) => {
if (!show) return null;

return (
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 flex items-center justify-center">
<div className="bg-white p-6 rounded-lg shadow-lg">
<h3 className="text-lg font-semibold mb-4">{title}</h3>
<p className="mb-4">{description}</p>
<div className="flex justify-end">
<button
onClick={onConfirm}
className="bg-blue-500 text-white px-4 py-2 rounded"
>
Confirm
</button>
<button
onClick={onClose}
className="bg-gray-500 text-white px-4 py-2 rounded mr-2"
>
Cancel
</button>
</div>
</div>
</div>
);
};

export default ConfirmationModal;
44 changes: 44 additions & 0 deletions src/components/ConfirmationModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from 'react';

interface ConfirmationModalProps {
show: boolean;
onClose: () => void;
onConfirm: () => void;
title: string;
description: string;
}

const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
show,
onClose,
onConfirm,
title,
description,
}) => {
if (!show) return null;

return (
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 flex items-center justify-center">
<div className="bg-white p-6 rounded-lg shadow-lg">
<h3 className="text-lg font-semibold mb-4">{title}</h3>
<p className="mb-4">{description}</p>
<div className="flex justify-end">
<button
onClick={onConfirm}
className="bg-blue-500 text-white px-4 py-2 rounded"
>
Confirm
</button>
<button
onClick={onClose}
className="bg-gray-500 text-white px-4 py-2 rounded mr-2"
>
Cancel
</button>
</div>
</div>
</div>
);
};

export default ConfirmationModal;
5 changes: 3 additions & 2 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ import Link from "next/link";
import { useContext } from "react";

export default function Header() {
const { user } = useContext(UserContext);
const { user, usertype } = useContext(UserContext);

return (
<header className="flex justify-between items-center p-4 bg-gray-800 text-white">
<Link href={"/"} className="text-2xl font-bold">[CC] Turismo</Link>
<Link href={"/"} className="text-2xl font-bold">Trek Trill</Link>
<nav>
<ul className="flex space-x-4">
<li><Link href="/">Home</Link></li>
{usertype == "host" ? <li><Link href="/my_accommodations">My Accommodations</Link></li> : null }
<li><Link href="/about">About Us</Link></li>
{Object.keys(user).length > 0 ?
<li className="cursor-pointer" onClick={() => auth.signOut()}>Sign out</li>
Expand Down
18 changes: 18 additions & 0 deletions src/components/usertype_check/AdminCheck.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use client';

import { UserContext } from "@/lib/context";
import { useRouter } from "next/navigation";
import { useContext, useEffect } from "react";

export default function AdminCheck(props: {children: React.ReactNode}) {
const { user, usertype } = useContext(UserContext);
const router = useRouter();

useEffect(() => {
if (Object.keys(user).length === 0){
router.push('/');
}
}, [user, usertype]);

return user && usertype == "admin" ? <>{props.children}</> : null;
}
Loading

0 comments on commit 31307e5

Please sign in to comment.