Skip to content

Commit

Permalink
feat: add review form stars, comments and upload image
Browse files Browse the repository at this point in the history
  • Loading branch information
farhan-helmy committed Feb 18, 2023
1 parent d20c447 commit 95f0d57
Show file tree
Hide file tree
Showing 12 changed files with 905 additions and 121 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@prisma/client": "^4.9.0",
"@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.9",
"@tanstack/react-query": "^4.20.0",
"@trpc/client": "^10.9.0",
"@trpc/next": "^10.9.0",
Expand All @@ -25,6 +26,7 @@
"next-auth": "^4.19.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-select": "^5.7.0",
"superjson": "1.9.1",
"zod": "^3.20.2"
},
Expand Down
32 changes: 32 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,37 @@ model Example {
updatedAt DateTime @updatedAt
}

model Surau {
id String @id @default(cuid())
name String
address String
city String
state String
zip_code String
ratings Rating[]
}

model Rating {
id String @id @default(cuid())
rating Int
review String?
created_at DateTime @default(now())
surau Surau @relation(fields: [id], references: [id])
place_id Int
user User @relation(fields: [user_id], references: [id])
user_id String
photos Photo[]
}

model Photo {
photo_id String @id @default(cuid())
file_path String
caption String?
created_at DateTime @default(now())
rating Rating @relation(fields: [rating_id], references: [id])
rating_id String
}

// Necessary for Next auth
model Account {
id String @id @default(cuid())
Expand Down Expand Up @@ -55,6 +86,7 @@ model User {
image String?
accounts Account[]
sessions Session[]
ratings Rating[]
}

model VerificationToken {
Expand Down
133 changes: 133 additions & 0 deletions src/components/AddSurauForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-return */
import dynamic from 'next/dynamic'
import type { FC} from 'react';
import { useEffect, useState } from 'react'
const Select = dynamic(() => import("react-select"), {
ssr: true,
})

type State = {
administrative_division: string;
state: string
capital: string,
royal_capital: string,
population: number,
total_area: number,
licence_plate_prefix: string,
phone_area_code: string,
abbreviation: string,
ISO: string,
FIPS: string,
HDI: number,
region: string,
head_of_state: string,
head_of_goverment: string
}

export type AddSurauFormProps = {
open: boolean,
setOpen: (open: boolean) => void
}

const AddSurauForm: FC<AddSurauFormProps> = ({ open, setOpen }) => {

const [state, setState] = useState<State[]>([]);

useEffect(() => {
void fetch("https://jianliew.me/malaysia-api/state/v1/all.json")
.then(res => res.json())
.then(data => {
console.log(data)
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
setState(data)
})
}, [])

return (
<>
<div className="">
<div className="md:grid md:grid-cols-2 md:gap-6">
<div className="md:col-span-1">
<div className="px-4 sm:px-0">
<h3 className="text-lg font-medium leading-6 text-gray-900">Add surau</h3>
<p className="mt-1 text-gray-600 text-xs italic">
Help us to add surau if it is not in the list.
</p>
</div>
</div>
<div className="mt-4 md:col-span-2 md:mt-0">
<form action="#" method="POST">
<div className="shadow sm:overflow-hidden sm:rounded-md">
<div className="space-y-6 bg-white px-4 py-5 sm:p-6">
<div className="grid grid-cols-3 gap-6">
<div className="col-span-3 sm:col-span-2">
<label htmlFor="surau-name" className="block text-sm font-medium text-gray-700">
Surau Name
</label>
<div className="mt-1 flex rounded-md shadow-sm">
<input
type="text"
name="surau-name"
id="surau-name"
className="block w-full flex-1 rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
placeholder=""
/>
</div>
</div>
</div>
<div className="grid grid-cols-3 gap-6">
<div className="col-span-2 sm:col-span-2">
<label htmlFor="surau-name" className="block text-sm font-medium text-gray-700">
State
</label>
<div className="mt-1 block rounded-md shadow-sm w-full">
<Select
options={state}
getOptionLabel={(option: any) => option.state}
getOptionValue={(option: any) => option.state}
/>
</div>
</div>
</div>
<div>
<label htmlFor="about" className="block text-sm font-medium text-gray-700">
Direction / guide
</label>
<div className="mt-1">
<textarea
id="about"
name="about"
rows={3}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
defaultValue={''}
/>
</div>
<p className="mt-2 text-sm text-gray-500">
Brief direction or guide to the surau. eg. near to the mosque, near to the shop lot, etc.
</p>
</div>


</div>
<div className="bg-gray-50 px-4 py-3 text-right sm:px-6 flex flex-row items-end justify-end gap-2">
<button
type="submit"
className=" justify-center rounded-md border border-transparent bg-indigo-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
Save
</button>
<div className="mb-2 font-light underline" onClick={() => setOpen(false)}>Close</div>
</div>
</div>
</form>
</div>
</div>
</div>


</>
)
}

export default AddSurauForm;
50 changes: 50 additions & 0 deletions src/components/AddSurauFormModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { FC} from 'react';
import { Fragment, useState } from 'react'
import { Dialog, Transition } from '@headlessui/react'
import { CheckIcon } from '@heroicons/react/24/outline'
import type { AddSurauFormProps } from './AddSurauForm';
import AddSurauForm from './AddSurauForm'

type AddSurauFormModalProps = AddSurauFormProps

const AddSurauFormModal: FC<AddSurauFormModalProps> = ({open, setOpen}) => {
return (
<Transition.Root show={open} as={Fragment}>
<Dialog as="div" className="relative z-10" onClose={setOpen}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>

<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-4xl sm:p-6">
<div>
<AddSurauForm setOpen={setOpen} open />
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
)
}

export default AddSurauFormModal;
144 changes: 144 additions & 0 deletions src/components/ReviewSurauForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { StarIcon } from "@heroicons/react/20/solid";
import Image from "next/image";
import { FC, useEffect } from "react";
import { useState } from "react"


export type ReviewSurauFormProps = {
open: boolean,
setOpen: (open: boolean) => void
surauName: string
}

type ImagePreviews = {
url: string,
}
const ReviewSurauForm: FC<ReviewSurauFormProps> = ({ open, setOpen, surauName }) => {

const [rating, setRating] = useState(0);
const [imagePreviews, setImagePreviews] = useState<ImagePreviews[]>();

const handleRatingChange = (newRating: number) => {
setRating(newRating);
};

const renderStars = () => {
const stars = [];
for (let i = 1; i <= 5; i++) {
stars.push(
<StarIcon
key={i}
className={`w-6 h-6 ${i <= rating ? "text-yellow-500" : "text-gray-400"
} cursor-pointer`}
onClick={() => handleRatingChange(i)}
/>
);
}

return stars;
};

const selectImages = (e: React.ChangeEvent<HTMLInputElement>) => {
const images: ImagePreviews[] = [];

if (e.target.files === null) return;

for (let i = 0; i < e.target.files.length; i++) {
images.push(URL.createObjectURL(e.target.files[i] as Blob) as unknown as ImagePreviews);
}

setImagePreviews(images);
}

return (
<div className="">
<div className="md:grid md:grid-cols-2 md:gap-6">
<div className="md:col-span-1">
<div className="px-4 sm:px-0">
<h3 className="text-lg font-medium leading-6 text-gray-900">Review</h3>
<p className="mt-1 text-gray-600 text-xs italic">
Review this {surauName} surau inshaAllah
</p>
</div>
</div>
<div className="mt-4 md:col-span-2 md:mt-0">

<div className="shadow sm:overflow-hidden sm:rounded-md">
<div className="flex items-center justify-center pt-4">
{renderStars()}
</div>
<div className="space-y-6 bg-white px-4 py-5 sm:p-6">
<div>
<label htmlFor="about" className="block text-sm font-medium text-gray-700">
</label>
<div className="mt-1">
<textarea
id="about"
name="about"
rows={3}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
defaultValue={''}
/>
</div>
<p className="mt-2 text-sm text-gray-500">
Add your honest review about this surau and also any improvement that can be made.
</p>
</div>
<div className="mt-1 sm:col-span-2 sm:mt-0">
<div className="flex max-w-lg justify-center rounded-md border-2 border-dashed border-gray-300 px-6 pt-5 pb-6">
<div className="space-y-1 text-center">
<svg
className="mx-auto h-12 w-12 text-gray-400"
stroke="currentColor"
fill="none"
viewBox="0 0 48 48"
aria-hidden="true"
>
<path
d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
<div className="flex text-sm text-gray-600">
<label
className="relative cursor-pointer rounded-md bg-white font-medium text-indigo-600 focus-within:outline-none focus-within:ring-2 focus-within:ring-indigo-500 focus-within:ring-offset-2 hover:text-indigo-500"
>
<span>Upload a file</span>
<input onChange={selectImages} name="file-upload" type="file" className="sr-only" multiple accept="image/*" />
</label>
<p className="pl-1">or drag and drop</p>
</div>
<p className="text-xs text-gray-500">PNG, JPG, GIF up to 10MB</p>
</div>
</div>
</div>
<div className="flex flex-row gap-2">
{imagePreviews ?(
imagePreviews.map((imagePreview, index) => (
<div key={index} className="">
<Image src={imagePreview as unknown as string} alt="xsd" height={250} width={250} />
</div>
)
)) : null}
</div>
</div>
<div className="bg-gray-50 px-4 py-3 text-right sm:px-6 flex flex-row items-end justify-end gap-2">
<button
className=" justify-center rounded-md border border-transparent bg-indigo-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
Submit Review
</button>
<div className="mb-2 font-light underline" onClick={() => setOpen(false)}>Close</div>
</div>
</div>

</div>
</div>
</div>
)

}

export default ReviewSurauForm
Loading

1 comment on commit 95f0d57

@vercel
Copy link

@vercel vercel bot commented on 95f0d57 Feb 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.