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 |
--------------------------------------------------------------------------------
/app/admin-dashboard/properties-table.tsx:
--------------------------------------------------------------------------------
1 | import PropertyStatusBadge from "@/components/property-status-badge";
2 | import { Button } from "@/components/ui/button";
3 | import {
4 | Table,
5 | TableBody,
6 | TableCell,
7 | TableFooter,
8 | TableHead,
9 | TableHeader,
10 | TableRow,
11 | } from "@/components/ui/table";
12 | import { getProperties } from "@/data/properties";
13 | import { EyeIcon, PencilIcon } from "lucide-react";
14 | import Link from "next/link";
15 | import numeral from "numeral";
16 |
17 | export default async function PropertiesTable({ page = 1 }: { page?: number }) {
18 | const { data, totalPages } = await getProperties({
19 | pagination: {
20 | page,
21 | pageSize: 2,
22 | },
23 | });
24 | return (
25 | <>
26 | {!data && (
27 |
28 | You have no properties
29 |
30 | )}
31 | {!!data && (
32 |
33 |
34 |
35 | Address
36 | Listing Price
37 | Status
38 |
39 |
40 |
41 |
42 | {data.map((property) => {
43 | const address = [
44 | property.address1,
45 | property.address2,
46 | property.city,
47 | property.postcode,
48 | ]
49 | .filter((addressLine) => !!addressLine)
50 | .join(", ");
51 |
52 | return (
53 |
54 | {address}
55 |
56 | £{numeral(property.price).format("0,0")}
57 |
58 |
59 |
60 |
61 |
62 |
67 |
72 |
73 |
74 | );
75 | })}
76 |
77 |
78 |
79 |
80 | {Array.from({ length: totalPages }).map((_, i) => (
81 |
90 | ))}
91 |
92 |
93 |
94 |
95 | )}
96 | >
97 | );
98 | }
99 |
--------------------------------------------------------------------------------
/app/property-search/filters-form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import {
5 | Form,
6 | FormControl,
7 | FormField,
8 | FormItem,
9 | FormLabel,
10 | } from "@/components/ui/form";
11 | import { Input } from "@/components/ui/input";
12 | import { zodResolver } from "@hookform/resolvers/zod";
13 | import { useRouter, useSearchParams } from "next/navigation";
14 | import { useForm } from "react-hook-form";
15 | import { z } from "zod";
16 |
17 | const formSchema = z.object({
18 | minPrice: z.string().optional(),
19 | maxPrice: z.string().optional(),
20 | minBedrooms: z.string().optional(),
21 | });
22 |
23 | export default function FiltersForm() {
24 | const router = useRouter();
25 | const searchParams = useSearchParams();
26 |
27 | const form = useForm>({
28 | resolver: zodResolver(formSchema),
29 | defaultValues: {
30 | maxPrice: searchParams.get("maxPrice") ?? "",
31 | minBedrooms: searchParams.get("minBedrooms") ?? "",
32 | minPrice: searchParams.get("minPrice") ?? "",
33 | },
34 | });
35 |
36 | const handleSubmit = async (data: z.infer) => {
37 | console.log({ data });
38 | const newSearchParams = new URLSearchParams();
39 |
40 | if (data.minPrice) {
41 | newSearchParams.set("minPrice", data.minPrice);
42 | }
43 |
44 | if (data.maxPrice) {
45 | newSearchParams.set("maxPrice", data.maxPrice);
46 | }
47 |
48 | if (data.minBedrooms) {
49 | newSearchParams.set("minBedrooms", data.minBedrooms);
50 | }
51 |
52 | newSearchParams.set("page", "1");
53 | router.push(`/property-search?${newSearchParams.toString()}`);
54 | };
55 |
56 | return (
57 |
117 |
118 | );
119 | }
120 |
--------------------------------------------------------------------------------
/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 | import Link from "next/link";
7 |
8 | const Breadcrumbs = ({
9 | items,
10 | }: {
11 | items: {
12 | href?: string;
13 | label: string;
14 | }[];
15 | }) => {
16 | return (
17 |
18 |
19 | {items.map((item, i) => (
20 |
21 |
22 | {!!item.href && {item.label}}
23 | {!item.href && {item.label}}
24 |
25 | {i < items.length - 1 && }
26 |
27 | ))}
28 |
29 |
30 | );
31 | };
32 | Breadcrumbs.displayName = "Breadcrumbs";
33 |
34 | const Breadcrumb = React.forwardRef<
35 | HTMLElement,
36 | React.ComponentPropsWithoutRef<"nav"> & {
37 | separator?: React.ReactNode;
38 | }
39 | >(({ ...props }, ref) => );
40 | Breadcrumb.displayName = "Breadcrumb";
41 |
42 | const BreadcrumbList = React.forwardRef<
43 | HTMLOListElement,
44 | React.ComponentPropsWithoutRef<"ol">
45 | >(({ className, ...props }, ref) => (
46 |
54 | ));
55 | BreadcrumbList.displayName = "BreadcrumbList";
56 |
57 | const BreadcrumbItem = React.forwardRef<
58 | HTMLLIElement,
59 | React.ComponentPropsWithoutRef<"li">
60 | >(({ className, ...props }, ref) => (
61 |
66 | ));
67 | BreadcrumbItem.displayName = "BreadcrumbItem";
68 |
69 | const BreadcrumbLink = React.forwardRef<
70 | HTMLAnchorElement,
71 | React.ComponentPropsWithoutRef<"a"> & {
72 | asChild?: boolean;
73 | }
74 | >(({ asChild, className, ...props }, ref) => {
75 | const Comp = asChild ? Slot : "a";
76 |
77 | return (
78 |
83 | );
84 | });
85 | BreadcrumbLink.displayName = "BreadcrumbLink";
86 |
87 | const BreadcrumbPage = React.forwardRef<
88 | HTMLSpanElement,
89 | React.ComponentPropsWithoutRef<"span">
90 | >(({ className, ...props }, ref) => (
91 |
99 | ));
100 | BreadcrumbPage.displayName = "BreadcrumbPage";
101 |
102 | const BreadcrumbSeparator = ({
103 | children,
104 | className,
105 | ...props
106 | }: React.ComponentProps<"li">) => (
107 | svg]:w-3.5 [&>svg]:h-3.5", className)}
111 | {...props}
112 | >
113 | {children ?? }
114 |
115 | );
116 | BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
117 |
118 | const BreadcrumbEllipsis = ({
119 | className,
120 | ...props
121 | }: React.ComponentProps<"span">) => (
122 |
128 |
129 | More
130 |
131 | );
132 | BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
133 |
134 | export {
135 | Breadcrumbs,
136 | Breadcrumb,
137 | BreadcrumbList,
138 | BreadcrumbItem,
139 | BreadcrumbLink,
140 | BreadcrumbPage,
141 | BreadcrumbSeparator,
142 | BreadcrumbEllipsis,
143 | };
144 |
--------------------------------------------------------------------------------
/app/(auth)/register/register-form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import ContinueWithGoogleButton from "@/components/continue-with-google-button";
4 | import { Button } from "@/components/ui/button";
5 | import {
6 | Form,
7 | FormControl,
8 | FormField,
9 | FormItem,
10 | FormLabel,
11 | FormMessage,
12 | } from "@/components/ui/form";
13 | import { Input } from "@/components/ui/input";
14 | import { registerUserSchema } from "@/validation/registerUser";
15 | import { zodResolver } from "@hookform/resolvers/zod";
16 | import { useForm } from "react-hook-form";
17 | import { z } from "zod";
18 | import { registerUser } from "./actions";
19 | import { useRouter } from "next/navigation";
20 | import { toast } from "sonner";
21 |
22 | export default function RegisterForm() {
23 | const router = useRouter();
24 |
25 | const form = useForm>({
26 | resolver: zodResolver(registerUserSchema),
27 | defaultValues: {
28 | email: "",
29 | password: "",
30 | passwordConfirm: "",
31 | name: "",
32 | },
33 | });
34 |
35 | const handleSubmit = async (data: z.infer) => {
36 | const response = await registerUser(data);
37 |
38 | if (!!response?.error) {
39 | toast.error("Error!", {
40 | description: response.message,
41 | });
42 | return;
43 | }
44 |
45 | toast.success("Success!", {
46 | description: "Your account was created successfully!",
47 | });
48 |
49 | router.push("/login");
50 | };
51 |
52 | return (
53 |
127 |
128 |
129 | );
130 | }
131 |
--------------------------------------------------------------------------------
/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DialogPrimitive from "@radix-ui/react-dialog"
5 | import { cn } from "@/lib/utils"
6 | import { Cross2Icon } from "@radix-ui/react-icons"
7 |
8 | const Dialog = DialogPrimitive.Root
9 |
10 | const DialogTrigger = DialogPrimitive.Trigger
11 |
12 | const DialogPortal = DialogPrimitive.Portal
13 |
14 | const DialogClose = DialogPrimitive.Close
15 |
16 | const DialogOverlay = React.forwardRef<
17 | React.ElementRef,
18 | React.ComponentPropsWithoutRef
19 | >(({ className, ...props }, ref) => (
20 |
28 | ))
29 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
30 |
31 | const DialogContent = React.forwardRef<
32 | React.ElementRef,
33 | React.ComponentPropsWithoutRef
34 | >(({ className, children, ...props }, ref) => (
35 |
36 |
37 |
45 | {children}
46 |
47 |
48 | Close
49 |
50 |
51 |
52 | ))
53 | DialogContent.displayName = DialogPrimitive.Content.displayName
54 |
55 | const DialogHeader = ({
56 | className,
57 | ...props
58 | }: React.HTMLAttributes) => (
59 |
66 | )
67 | DialogHeader.displayName = "DialogHeader"
68 |
69 | const DialogFooter = ({
70 | className,
71 | ...props
72 | }: React.HTMLAttributes) => (
73 |
80 | )
81 | DialogFooter.displayName = "DialogFooter"
82 |
83 | const DialogTitle = React.forwardRef<
84 | React.ElementRef,
85 | React.ComponentPropsWithoutRef
86 | >(({ className, ...props }, ref) => (
87 |
95 | ))
96 | DialogTitle.displayName = DialogPrimitive.Title.displayName
97 |
98 | const DialogDescription = React.forwardRef<
99 | React.ElementRef,
100 | React.ComponentPropsWithoutRef
101 | >(({ className, ...props }, ref) => (
102 |
107 | ))
108 | DialogDescription.displayName = DialogPrimitive.Description.displayName
109 |
110 | export {
111 | Dialog,
112 | DialogPortal,
113 | DialogOverlay,
114 | DialogTrigger,
115 | DialogClose,
116 | DialogContent,
117 | DialogHeader,
118 | DialogFooter,
119 | DialogTitle,
120 | DialogDescription,
121 | }
122 |
--------------------------------------------------------------------------------
/app/account/my-favourites/page.tsx:
--------------------------------------------------------------------------------
1 | import PropertyStatusBadge from "@/components/property-status-badge";
2 | import { Button } from "@/components/ui/button";
3 | import {
4 | Table,
5 | TableBody,
6 | TableCell,
7 | TableFooter,
8 | TableHead,
9 | TableHeader,
10 | TableRow,
11 | } from "@/components/ui/table";
12 | import { getUserFavourites } from "@/data/favourites";
13 | import { getPropertiesById } from "@/data/properties";
14 | import { EyeIcon, Trash2Icon } from "lucide-react";
15 | import Link from "next/link";
16 | import RemoveFavouriteButton from "./remove-favourite-button";
17 | import { redirect } from "next/navigation";
18 |
19 | export default async function MyFavourites({
20 | searchParams,
21 | }: {
22 | searchParams: Promise;
23 | }) {
24 | const searchParamsValues = await searchParams;
25 | const page = searchParamsValues?.page ? parseInt(searchParamsValues.page) : 1;
26 | const pageSize = 2;
27 | const favourites = await getUserFavourites();
28 | const allFavourites = Object.keys(favourites);
29 | const totalPages = Math.ceil(allFavourites.length / pageSize);
30 |
31 | const paginatedFavourites = allFavourites.slice(
32 | (page - 1) * pageSize,
33 | page * pageSize
34 | );
35 |
36 | if (!paginatedFavourites.length && page > 1) {
37 | redirect(`/account/my-favourites?page=${totalPages}`);
38 | }
39 |
40 | const properties = await getPropertiesById(paginatedFavourites);
41 | console.log({ paginatedFavourites, properties });
42 |
43 | return (
44 |
45 | My favourites
46 | {!paginatedFavourites.length && (
47 |
48 | You have no favourited properties.
49 |
50 | )}
51 | {!!paginatedFavourites.length && (
52 |
53 |
54 |
55 | Property
56 | Status
57 |
58 |
59 |
60 |
61 | {paginatedFavourites.map((favourite) => {
62 | const property = properties.find(
63 | (property) => property.id === favourite
64 | );
65 | const address = [
66 | property?.address1,
67 | property?.address2,
68 | property?.city,
69 | property?.postcode,
70 | ]
71 | .filter((addressLine) => !!addressLine)
72 | .join(", ");
73 |
74 | return (
75 |
76 | {address}
77 |
78 | {!!property && (
79 |
80 | )}
81 |
82 |
83 | {!!property && (
84 | <>
85 |
90 |
91 | >
92 | )}
93 |
94 |
95 | );
96 | })}
97 |
98 |
99 |
100 |
101 | {Array.from({ length: totalPages }).map((_, i) => {
102 | return (
103 |
114 | );
115 | })}
116 |
117 |
118 |
119 |
120 | )}
121 |
122 | );
123 | }
124 |
--------------------------------------------------------------------------------
/app/account/update-password-form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import {
5 | Form,
6 | FormControl,
7 | FormField,
8 | FormItem,
9 | FormLabel,
10 | FormMessage,
11 | } from "@/components/ui/form";
12 | import { Input } from "@/components/ui/input";
13 | import { useAuth } from "@/context/auth";
14 | import { passwordValidation } from "@/validation/registerUser";
15 | import { zodResolver } from "@hookform/resolvers/zod";
16 | import {
17 | EmailAuthProvider,
18 | reauthenticateWithCredential,
19 | updatePassword,
20 | } from "firebase/auth";
21 | import { useForm } from "react-hook-form";
22 | import { toast } from "sonner";
23 | import { z } from "zod";
24 |
25 | const formSchema = z
26 | .object({
27 | currentPassword: passwordValidation,
28 | newPassword: passwordValidation,
29 | newPasswordConfirm: z.string(),
30 | })
31 | .superRefine((data, ctx) => {
32 | if (data.newPassword !== data.newPasswordConfirm) {
33 | ctx.addIssue({
34 | message: "Passwords do not match",
35 | path: ["newPasswordConfirm"],
36 | code: "custom",
37 | });
38 | }
39 | });
40 |
41 | export default function UpdatePasswordForm() {
42 | const auth = useAuth();
43 | const form = useForm>({
44 | resolver: zodResolver(formSchema),
45 | defaultValues: {
46 | currentPassword: "",
47 | newPassword: "",
48 | newPasswordConfirm: "",
49 | },
50 | });
51 |
52 | const handleSubmit = async (data: z.infer) => {
53 | const user = auth?.currentUser;
54 | if (!user?.email) {
55 | return;
56 | }
57 |
58 | try {
59 | await reauthenticateWithCredential(
60 | user,
61 | EmailAuthProvider.credential(user.email, data.currentPassword)
62 | );
63 | await updatePassword(user, data.newPassword);
64 | toast.success("Password updated successfully");
65 | form.reset();
66 | } catch (e: any) {
67 | console.log({ e });
68 | toast(
69 | e.code === "auth/invalid-credential"
70 | ? "Your current password is incorrect"
71 | : "An error occurred"
72 | );
73 | }
74 | };
75 |
76 | return (
77 |
78 | Update Password
79 |
139 |
140 |
141 | );
142 | }
143 |
--------------------------------------------------------------------------------
/components/multi-image-uploader.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useCallback, useRef } from "react";
4 | import { Button } from "./ui/button";
5 | import {
6 | DragDropContext,
7 | Draggable,
8 | Droppable,
9 | DropResult,
10 | } from "@hello-pangea/dnd";
11 | import Image from "next/image";
12 | import { Badge } from "./ui/badge";
13 | import { MoveIcon, XIcon } from "lucide-react";
14 |
15 | export type ImageUpload = {
16 | id: string;
17 | url: string;
18 | file?: File;
19 | };
20 |
21 | type Props = {
22 | images?: ImageUpload[];
23 | onImagesChange: (images: ImageUpload[]) => void;
24 | urlFormatter?: (image: ImageUpload) => string;
25 | };
26 |
27 | export default function MultiImageUploader({
28 | images = [],
29 | onImagesChange,
30 | urlFormatter,
31 | }: Props) {
32 | const uploadInputRef = useRef(null);
33 |
34 | console.log({ images });
35 |
36 | const handleInputChange = (e: React.ChangeEvent) => {
37 | const files = Array.from(e.target.files || []);
38 | const newImages = files.map((file, index) => {
39 | return {
40 | id: `${Date.now()}-${index}-${file.name}`,
41 | url: URL.createObjectURL(file),
42 | file,
43 | };
44 | });
45 |
46 | onImagesChange([...images, ...newImages]);
47 | };
48 |
49 | const handleDragEnd = useCallback(
50 | (result: DropResult) => {
51 | if (!result.destination) {
52 | return;
53 | }
54 |
55 | const items = Array.from(images);
56 | const [reorderedImage] = items.splice(result.source.index, 1);
57 | items.splice(result.destination.index, 0, reorderedImage);
58 | onImagesChange(items);
59 | },
60 | [onImagesChange, images]
61 | );
62 |
63 | const handleDelete = useCallback(
64 | (id: string) => {
65 | const updatedImages = images.filter((image) => image.id !== id);
66 | onImagesChange(updatedImages);
67 | },
68 | [onImagesChange, images]
69 | );
70 |
71 | return (
72 |
73 |
81 |
89 |
90 |
91 | {(provided) => (
92 |
93 | {images.map((image, index) => (
94 |
95 | {(provided) => (
96 |
102 |
103 |
104 |
110 |
111 |
112 |
113 | Image {index + 1}
114 |
115 | {index === 0 && (
116 | Featured Image
117 | )}
118 |
119 |
120 |
126 |
127 |
128 |
129 |
130 |
131 |
132 | )}
133 |
134 | ))}
135 | {provided.placeholder}
136 |
137 | )}
138 |
139 |
140 |
141 | );
142 | }
143 |
--------------------------------------------------------------------------------
/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { Slot } from "@radix-ui/react-slot"
6 | import {
7 | Controller,
8 | ControllerProps,
9 | FieldPath,
10 | FieldValues,
11 | FormProvider,
12 | useFormContext,
13 | } from "react-hook-form"
14 |
15 | import { cn } from "@/lib/utils"
16 | import { Label } from "@/components/ui/label"
17 |
18 | const Form = FormProvider
19 |
20 | type FormFieldContextValue<
21 | TFieldValues extends FieldValues = FieldValues,
22 | TName extends FieldPath = FieldPath
23 | > = {
24 | name: TName
25 | }
26 |
27 | const FormFieldContext = React.createContext(
28 | {} as FormFieldContextValue
29 | )
30 |
31 | const FormField = <
32 | TFieldValues extends FieldValues = FieldValues,
33 | TName extends FieldPath = FieldPath
34 | >({
35 | ...props
36 | }: ControllerProps) => {
37 | return (
38 |
39 |
40 |
41 | )
42 | }
43 |
44 | const useFormField = () => {
45 | const fieldContext = React.useContext(FormFieldContext)
46 | const itemContext = React.useContext(FormItemContext)
47 | const { getFieldState, formState } = useFormContext()
48 |
49 | const fieldState = getFieldState(fieldContext.name, formState)
50 |
51 | if (!fieldContext) {
52 | throw new Error("useFormField should be used within ")
53 | }
54 |
55 | const { id } = itemContext
56 |
57 | return {
58 | id,
59 | name: fieldContext.name,
60 | formItemId: `${id}-form-item`,
61 | formDescriptionId: `${id}-form-item-description`,
62 | formMessageId: `${id}-form-item-message`,
63 | ...fieldState,
64 | }
65 | }
66 |
67 | type FormItemContextValue = {
68 | id: string
69 | }
70 |
71 | const FormItemContext = React.createContext(
72 | {} as FormItemContextValue
73 | )
74 |
75 | const FormItem = React.forwardRef<
76 | HTMLDivElement,
77 | React.HTMLAttributes
78 | >(({ className, ...props }, ref) => {
79 | const id = React.useId()
80 |
81 | return (
82 |
83 |
84 |
85 | )
86 | })
87 | FormItem.displayName = "FormItem"
88 |
89 | const FormLabel = React.forwardRef<
90 | React.ElementRef,
91 | React.ComponentPropsWithoutRef
92 | >(({ className, ...props }, ref) => {
93 | const { error, formItemId } = useFormField()
94 |
95 | return (
96 |
102 | )
103 | })
104 | FormLabel.displayName = "FormLabel"
105 |
106 | const FormControl = React.forwardRef<
107 | React.ElementRef,
108 | React.ComponentPropsWithoutRef
109 | >(({ ...props }, ref) => {
110 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
111 |
112 | return (
113 |
124 | )
125 | })
126 | FormControl.displayName = "FormControl"
127 |
128 | const FormDescription = React.forwardRef<
129 | HTMLParagraphElement,
130 | React.HTMLAttributes
131 | >(({ className, ...props }, ref) => {
132 | const { formDescriptionId } = useFormField()
133 |
134 | return (
135 |
141 | )
142 | })
143 | FormDescription.displayName = "FormDescription"
144 |
145 | const FormMessage = React.forwardRef<
146 | HTMLParagraphElement,
147 | React.HTMLAttributes
148 | >(({ className, children, ...props }, ref) => {
149 | const { error, formMessageId } = useFormField()
150 | const body = error ? String(error?.message) : children
151 |
152 | if (!body) {
153 | return null
154 | }
155 |
156 | return (
157 |
163 | {body}
164 |
165 | )
166 | })
167 | FormMessage.displayName = "FormMessage"
168 |
169 | export {
170 | useFormField,
171 | Form,
172 | FormItem,
173 | FormLabel,
174 | FormControl,
175 | FormDescription,
176 | FormMessage,
177 | FormField,
178 | }
179 |
--------------------------------------------------------------------------------
/components/ui/alert-dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
5 |
6 | import { cn } from "@/lib/utils"
7 | import { buttonVariants } from "@/components/ui/button"
8 |
9 | const AlertDialog = AlertDialogPrimitive.Root
10 |
11 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger
12 |
13 | const AlertDialogPortal = AlertDialogPrimitive.Portal
14 |
15 | const AlertDialogOverlay = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, ...props }, ref) => (
19 |
27 | ))
28 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
29 |
30 | const AlertDialogContent = React.forwardRef<
31 | React.ElementRef,
32 | React.ComponentPropsWithoutRef
33 | >(({ className, ...props }, ref) => (
34 |
35 |
36 |
44 |
45 | ))
46 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
47 |
48 | const AlertDialogHeader = ({
49 | className,
50 | ...props
51 | }: React.HTMLAttributes) => (
52 |
59 | )
60 | AlertDialogHeader.displayName = "AlertDialogHeader"
61 |
62 | const AlertDialogFooter = ({
63 | className,
64 | ...props
65 | }: React.HTMLAttributes) => (
66 |
73 | )
74 | AlertDialogFooter.displayName = "AlertDialogFooter"
75 |
76 | const AlertDialogTitle = React.forwardRef<
77 | React.ElementRef,
78 | React.ComponentPropsWithoutRef
79 | >(({ className, ...props }, ref) => (
80 |
85 | ))
86 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
87 |
88 | const AlertDialogDescription = React.forwardRef<
89 | React.ElementRef,
90 | React.ComponentPropsWithoutRef
91 | >(({ className, ...props }, ref) => (
92 |
97 | ))
98 | AlertDialogDescription.displayName =
99 | AlertDialogPrimitive.Description.displayName
100 |
101 | const AlertDialogAction = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
112 |
113 | const AlertDialogCancel = React.forwardRef<
114 | React.ElementRef,
115 | React.ComponentPropsWithoutRef
116 | >(({ className, ...props }, ref) => (
117 |
126 | ))
127 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
128 |
129 | export {
130 | AlertDialog,
131 | AlertDialogPortal,
132 | AlertDialogOverlay,
133 | AlertDialogTrigger,
134 | AlertDialogContent,
135 | AlertDialogHeader,
136 | AlertDialogFooter,
137 | AlertDialogTitle,
138 | AlertDialogDescription,
139 | AlertDialogAction,
140 | AlertDialogCancel,
141 | }
142 |
--------------------------------------------------------------------------------
/app/property-search/page.tsx:
--------------------------------------------------------------------------------
1 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
2 | import FiltersForm from "./filters-form";
3 | import { Suspense } from "react";
4 | import { getProperties } from "@/data/properties";
5 | import Image from "next/image";
6 | import imageUrlFormatter from "@/lib/imageUrlFormatter";
7 | import { BathIcon, BedIcon, HomeIcon } from "lucide-react";
8 | import numeral from "numeral";
9 | import { Button } from "@/components/ui/button";
10 | import Link from "next/link";
11 | import ToggleFavouriteButton from "./toggle-favourite-button";
12 | import { getUserFavourites } from "@/data/favourites";
13 | import { cookies } from "next/headers";
14 | import { auth } from "@/firebase/server";
15 | import { DecodedIdToken } from "firebase-admin/auth";
16 |
17 | export default async function PropertySearch({
18 | searchParams,
19 | }: {
20 | searchParams: Promise;
21 | }) {
22 | const searchParamsValues = await searchParams;
23 |
24 | const parsedPage = parseInt(searchParamsValues?.page);
25 | const parsedMinPrice = parseInt(searchParamsValues?.minPrice);
26 | const parsedMaxPrice = parseInt(searchParamsValues?.maxPrice);
27 | const parsedMinBedrooms = parseInt(searchParamsValues?.minBedrooms);
28 |
29 | const page = isNaN(parsedPage) ? 1 : parsedPage;
30 | const minPrice = isNaN(parsedMinPrice) ? null : parsedMinPrice;
31 | const maxPrice = isNaN(parsedMaxPrice) ? null : parsedMaxPrice;
32 | const minBedrooms = isNaN(parsedMinBedrooms) ? null : parsedMinBedrooms;
33 |
34 | const { data, totalPages } = await getProperties({
35 | pagination: {
36 | page,
37 | pageSize: 3,
38 | },
39 | filters: {
40 | minPrice,
41 | maxPrice,
42 | minBedrooms,
43 | status: ["for-sale"],
44 | },
45 | });
46 |
47 | const userFavourites = await getUserFavourites();
48 |
49 | console.log({ userFavourites });
50 |
51 | const cookieStore = await cookies();
52 | const token = cookieStore.get("firebaseAuthToken")?.value;
53 | let verifiedToken: DecodedIdToken | null;
54 |
55 | if (token) {
56 | verifiedToken = await auth.verifyIdToken(token);
57 | }
58 |
59 | return (
60 |
61 | Property Search
62 |
63 |
64 | Filters
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | {data.map((property) => {
74 | const addressLines = [
75 | property.address1,
76 | property.address2,
77 | property.city,
78 | property.postcode,
79 | ]
80 | .filter((addressLine) => !!addressLine)
81 | .join(", ");
82 | return (
83 |
84 |
85 |
86 | {(!verifiedToken || !verifiedToken.admin) && (
87 |
91 | )}
92 | {!!property.images?.[0] && (
93 |
99 | )}
100 | {!property.images?.[0] && (
101 | <>
102 |
103 | No Image
104 | >
105 | )}
106 |
107 |
108 | {addressLines}
109 |
110 |
111 | {property.bedrooms}
112 |
113 |
114 | {property.bathrooms}
115 |
116 |
117 |
118 | £{numeral(property.price).format("0,0")}
119 |
120 |
123 |
124 |
125 |
126 | );
127 | })}
128 |
129 |
130 | {Array.from({ length: totalPages }).map((_, i) => {
131 | const newSearchParams = new URLSearchParams();
132 |
133 | if (searchParamsValues?.minPrice) {
134 | newSearchParams.set("minPrice", searchParamsValues.minPrice);
135 | }
136 |
137 | if (searchParamsValues?.maxPrice) {
138 | newSearchParams.set("maxPrice", searchParamsValues.maxPrice);
139 | }
140 |
141 | if (searchParamsValues?.minBedrooms) {
142 | newSearchParams.set("minBedrooms", searchParamsValues.minBedrooms);
143 | }
144 |
145 | newSearchParams.set("page", `${i + 1}`);
146 |
147 | return (
148 |
158 | );
159 | })}
160 |
161 |
162 | );
163 | }
164 |
--------------------------------------------------------------------------------
/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import {
5 | CaretSortIcon,
6 | CheckIcon,
7 | ChevronDownIcon,
8 | ChevronUpIcon,
9 | } from "@radix-ui/react-icons"
10 | import * as SelectPrimitive from "@radix-ui/react-select"
11 |
12 | import { cn } from "@/lib/utils"
13 |
14 | const Select = SelectPrimitive.Root
15 |
16 | const SelectGroup = SelectPrimitive.Group
17 |
18 | const SelectValue = SelectPrimitive.Value
19 |
20 | const SelectTrigger = React.forwardRef<
21 | React.ElementRef,
22 | React.ComponentPropsWithoutRef
23 | >(({ className, children, ...props }, ref) => (
24 | span]:line-clamp-1",
28 | className
29 | )}
30 | {...props}
31 | >
32 | {children}
33 |
34 |
35 |
36 |
37 | ))
38 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
39 |
40 | const SelectScrollUpButton = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 |
53 |
54 | ))
55 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
56 |
57 | const SelectScrollDownButton = React.forwardRef<
58 | React.ElementRef,
59 | React.ComponentPropsWithoutRef
60 | >(({ className, ...props }, ref) => (
61 |
69 |
70 |
71 | ))
72 | SelectScrollDownButton.displayName =
73 | SelectPrimitive.ScrollDownButton.displayName
74 |
75 | const SelectContent = React.forwardRef<
76 | React.ElementRef,
77 | React.ComponentPropsWithoutRef
78 | >(({ className, children, position = "popper", ...props }, ref) => (
79 |
80 |
91 |
92 |
99 | {children}
100 |
101 |
102 |
103 |
104 | ))
105 | SelectContent.displayName = SelectPrimitive.Content.displayName
106 |
107 | const SelectLabel = React.forwardRef<
108 | React.ElementRef,
109 | React.ComponentPropsWithoutRef
110 | >(({ className, ...props }, ref) => (
111 |
116 | ))
117 | SelectLabel.displayName = SelectPrimitive.Label.displayName
118 |
119 | const SelectItem = React.forwardRef<
120 | React.ElementRef,
121 | React.ComponentPropsWithoutRef
122 | >(({ className, children, ...props }, ref) => (
123 |
131 |
132 |
133 |
134 |
135 |
136 | {children}
137 |
138 | ))
139 | SelectItem.displayName = SelectPrimitive.Item.displayName
140 |
141 | const SelectSeparator = React.forwardRef<
142 | React.ElementRef,
143 | React.ComponentPropsWithoutRef
144 | >(({ className, ...props }, ref) => (
145 |
150 | ))
151 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
152 |
153 | export {
154 | Select,
155 | SelectGroup,
156 | SelectValue,
157 | SelectTrigger,
158 | SelectContent,
159 | SelectLabel,
160 | SelectItem,
161 | SelectSeparator,
162 | SelectScrollUpButton,
163 | SelectScrollDownButton,
164 | }
165 |
--------------------------------------------------------------------------------
/components/ui/carousel.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import useEmblaCarousel, {
5 | type UseEmblaCarouselType,
6 | } from "embla-carousel-react"
7 | import { cn } from "@/lib/utils"
8 | import { Button } from "@/components/ui/button"
9 | import { ArrowLeftIcon, ArrowRightIcon } from "@radix-ui/react-icons"
10 |
11 | type CarouselApi = UseEmblaCarouselType[1]
12 | type UseCarouselParameters = Parameters
13 | type CarouselOptions = UseCarouselParameters[0]
14 | type CarouselPlugin = UseCarouselParameters[1]
15 |
16 | type CarouselProps = {
17 | opts?: CarouselOptions
18 | plugins?: CarouselPlugin
19 | orientation?: "horizontal" | "vertical"
20 | setApi?: (api: CarouselApi) => void
21 | }
22 |
23 | type CarouselContextProps = {
24 | carouselRef: ReturnType[0]
25 | api: ReturnType[1]
26 | scrollPrev: () => void
27 | scrollNext: () => void
28 | canScrollPrev: boolean
29 | canScrollNext: boolean
30 | } & CarouselProps
31 |
32 | const CarouselContext = React.createContext(null)
33 |
34 | function useCarousel() {
35 | const context = React.useContext(CarouselContext)
36 |
37 | if (!context) {
38 | throw new Error("useCarousel must be used within a ")
39 | }
40 |
41 | return context
42 | }
43 |
44 | const Carousel = React.forwardRef<
45 | HTMLDivElement,
46 | React.HTMLAttributes & CarouselProps
47 | >(
48 | (
49 | {
50 | orientation = "horizontal",
51 | opts,
52 | setApi,
53 | plugins,
54 | className,
55 | children,
56 | ...props
57 | },
58 | ref
59 | ) => {
60 | const [carouselRef, api] = useEmblaCarousel(
61 | {
62 | ...opts,
63 | axis: orientation === "horizontal" ? "x" : "y",
64 | },
65 | plugins
66 | )
67 | const [canScrollPrev, setCanScrollPrev] = React.useState(false)
68 | const [canScrollNext, setCanScrollNext] = React.useState(false)
69 |
70 | const onSelect = React.useCallback((api: CarouselApi) => {
71 | if (!api) {
72 | return
73 | }
74 |
75 | setCanScrollPrev(api.canScrollPrev())
76 | setCanScrollNext(api.canScrollNext())
77 | }, [])
78 |
79 | const scrollPrev = React.useCallback(() => {
80 | api?.scrollPrev()
81 | }, [api])
82 |
83 | const scrollNext = React.useCallback(() => {
84 | api?.scrollNext()
85 | }, [api])
86 |
87 | const handleKeyDown = React.useCallback(
88 | (event: React.KeyboardEvent) => {
89 | if (event.key === "ArrowLeft") {
90 | event.preventDefault()
91 | scrollPrev()
92 | } else if (event.key === "ArrowRight") {
93 | event.preventDefault()
94 | scrollNext()
95 | }
96 | },
97 | [scrollPrev, scrollNext]
98 | )
99 |
100 | React.useEffect(() => {
101 | if (!api || !setApi) {
102 | return
103 | }
104 |
105 | setApi(api)
106 | }, [api, setApi])
107 |
108 | React.useEffect(() => {
109 | if (!api) {
110 | return
111 | }
112 |
113 | onSelect(api)
114 | api.on("reInit", onSelect)
115 | api.on("select", onSelect)
116 |
117 | return () => {
118 | api?.off("select", onSelect)
119 | }
120 | }, [api, onSelect])
121 |
122 | return (
123 |
136 |
144 | {children}
145 |
146 |
147 | )
148 | }
149 | )
150 | Carousel.displayName = "Carousel"
151 |
152 | const CarouselContent = React.forwardRef<
153 | HTMLDivElement,
154 | React.HTMLAttributes
155 | >(({ className, ...props }, ref) => {
156 | const { carouselRef, orientation } = useCarousel()
157 |
158 | return (
159 |
170 | )
171 | })
172 | CarouselContent.displayName = "CarouselContent"
173 |
174 | const CarouselItem = React.forwardRef<
175 | HTMLDivElement,
176 | React.HTMLAttributes
177 | >(({ className, ...props }, ref) => {
178 | const { orientation } = useCarousel()
179 |
180 | return (
181 |
192 | )
193 | })
194 | CarouselItem.displayName = "CarouselItem"
195 |
196 | const CarouselPrevious = React.forwardRef<
197 | HTMLButtonElement,
198 | React.ComponentProps
199 | >(({ className, variant = "outline", size = "icon", ...props }, ref) => {
200 | const { orientation, scrollPrev, canScrollPrev } = useCarousel()
201 |
202 | return (
203 |
221 | )
222 | })
223 | CarouselPrevious.displayName = "CarouselPrevious"
224 |
225 | const CarouselNext = React.forwardRef<
226 | HTMLButtonElement,
227 | React.ComponentProps
228 | >(({ className, variant = "outline", size = "icon", ...props }, ref) => {
229 | const { orientation, scrollNext, canScrollNext } = useCarousel()
230 |
231 | return (
232 |
250 | )
251 | })
252 | CarouselNext.displayName = "CarouselNext"
253 |
254 | export {
255 | type CarouselApi,
256 | Carousel,
257 | CarouselContent,
258 | CarouselItem,
259 | CarouselPrevious,
260 | CarouselNext,
261 | }
262 |
--------------------------------------------------------------------------------
/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5 | import {
6 | CheckIcon,
7 | ChevronRightIcon,
8 | DotFilledIcon,
9 | } from "@radix-ui/react-icons"
10 |
11 | import { cn } from "@/lib/utils"
12 |
13 | const DropdownMenu = DropdownMenuPrimitive.Root
14 |
15 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
16 |
17 | const DropdownMenuGroup = DropdownMenuPrimitive.Group
18 |
19 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal
20 |
21 | const DropdownMenuSub = DropdownMenuPrimitive.Sub
22 |
23 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
24 |
25 | const DropdownMenuSubTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef & {
28 | inset?: boolean
29 | }
30 | >(({ className, inset, children, ...props }, ref) => (
31 |
40 | {children}
41 |
42 |
43 | ))
44 | DropdownMenuSubTrigger.displayName =
45 | DropdownMenuPrimitive.SubTrigger.displayName
46 |
47 | const DropdownMenuSubContent = React.forwardRef<
48 | React.ElementRef,
49 | React.ComponentPropsWithoutRef
50 | >(({ className, ...props }, ref) => (
51 |
59 | ))
60 | DropdownMenuSubContent.displayName =
61 | DropdownMenuPrimitive.SubContent.displayName
62 |
63 | const DropdownMenuContent = React.forwardRef<
64 | React.ElementRef,
65 | React.ComponentPropsWithoutRef
66 | >(({ className, sideOffset = 4, ...props }, ref) => (
67 |
68 |
78 |
79 | ))
80 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
81 |
82 | const DropdownMenuItem = React.forwardRef<
83 | React.ElementRef,
84 | React.ComponentPropsWithoutRef & {
85 | inset?: boolean
86 | }
87 | >(({ className, inset, ...props }, ref) => (
88 | svg]:size-4 [&>svg]:shrink-0",
92 | inset && "pl-8",
93 | className
94 | )}
95 | {...props}
96 | />
97 | ))
98 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
99 |
100 | const DropdownMenuCheckboxItem = React.forwardRef<
101 | React.ElementRef,
102 | React.ComponentPropsWithoutRef
103 | >(({ className, children, checked, ...props }, ref) => (
104 |
113 |
114 |
115 |
116 |
117 |
118 | {children}
119 |
120 | ))
121 | DropdownMenuCheckboxItem.displayName =
122 | DropdownMenuPrimitive.CheckboxItem.displayName
123 |
124 | const DropdownMenuRadioItem = React.forwardRef<
125 | React.ElementRef,
126 | React.ComponentPropsWithoutRef
127 | >(({ className, children, ...props }, ref) => (
128 |
136 |
137 |
138 |
139 |
140 |
141 | {children}
142 |
143 | ))
144 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
145 |
146 | const DropdownMenuLabel = React.forwardRef<
147 | React.ElementRef,
148 | React.ComponentPropsWithoutRef & {
149 | inset?: boolean
150 | }
151 | >(({ className, inset, ...props }, ref) => (
152 |
161 | ))
162 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
163 |
164 | const DropdownMenuSeparator = React.forwardRef<
165 | React.ElementRef,
166 | React.ComponentPropsWithoutRef
167 | >(({ className, ...props }, ref) => (
168 |
173 | ))
174 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
175 |
176 | const DropdownMenuShortcut = ({
177 | className,
178 | ...props
179 | }: React.HTMLAttributes) => {
180 | return (
181 |
185 | )
186 | }
187 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
188 |
189 | export {
190 | DropdownMenu,
191 | DropdownMenuTrigger,
192 | DropdownMenuContent,
193 | DropdownMenuItem,
194 | DropdownMenuCheckboxItem,
195 | DropdownMenuRadioItem,
196 | DropdownMenuLabel,
197 | DropdownMenuSeparator,
198 | DropdownMenuShortcut,
199 | DropdownMenuGroup,
200 | DropdownMenuPortal,
201 | DropdownMenuSub,
202 | DropdownMenuSubContent,
203 | DropdownMenuSubTrigger,
204 | DropdownMenuRadioGroup,
205 | }
206 |
--------------------------------------------------------------------------------
/components/property-form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useForm } from "react-hook-form";
4 | import { propertySchema } from "@/validation/propertySchema";
5 | import { z } from "zod";
6 | import { zodResolver } from "@hookform/resolvers/zod";
7 | import {
8 | Form,
9 | FormControl,
10 | FormField,
11 | FormItem,
12 | FormLabel,
13 | FormMessage,
14 | } from "./ui/form";
15 | import {
16 | Select,
17 | SelectContent,
18 | SelectItem,
19 | SelectTrigger,
20 | SelectValue,
21 | } from "./ui/select";
22 | import { Input } from "./ui/input";
23 | import { Textarea } from "./ui/textarea";
24 | import { Button } from "./ui/button";
25 | import { Property } from "@/types/property";
26 | import MultiImageUploader, { ImageUpload } from "./multi-image-uploader";
27 |
28 | type Props = {
29 | submitButtonLabel: React.ReactNode;
30 | handleSubmit: (data: z.infer) => void;
31 | defaultValues?: z.infer;
32 | };
33 |
34 | export default function PropertyForm({
35 | handleSubmit,
36 | submitButtonLabel,
37 | defaultValues,
38 | }: Props) {
39 | const combinedDefaultValues: z.infer = {
40 | ...{
41 | address1: "",
42 | address2: "",
43 | city: "",
44 | postcode: "",
45 | price: 0,
46 | bedrooms: 0,
47 | bathrooms: 0,
48 | status: "draft",
49 | description: "",
50 | images: [],
51 | },
52 | ...defaultValues,
53 | };
54 |
55 | const form = useForm>({
56 | resolver: zodResolver(propertySchema),
57 | defaultValues: combinedDefaultValues,
58 | });
59 |
60 | return (
61 |
239 |
240 | );
241 | }
242 |
--------------------------------------------------------------------------------
|