toast, en, show number etc

This commit is contained in:
Zikil 2024-11-23 20:43:10 +07:00
parent d653c15c90
commit 53920d41ef
17 changed files with 461 additions and 105 deletions

View File

@ -6,13 +6,15 @@ import { MapPin, Calendar, Phone, MessageCircle, Share2, Flag, Heart } from 'luc
import { prisma } from '@/prisma/prisma-client';
import { notFound } from 'next/navigation';
import { ShowNumberModal } from '@/components/shared/modals/show-number';
import { getUserSession } from '@/lib/get-user-session';
type Params = Promise<{ id: string }>
export default async function AdtPage(props: { params: Params }) {
// const [ openShowNumberModal, setOpenShowNumberModal ] = React.useState(false)
const params = await props.params;
const session = await getUserSession();
const adt = await prisma.adt.findFirst({
where: {
id: Number(params.id),
@ -33,7 +35,6 @@ export default async function AdtPage(props: { params: Params }) {
return (
<>
{/* <ShowNumberModal open={openShowNumberModal} onClose={() => setOpenShowNumberModal(false)} /> */}
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
@ -91,13 +92,14 @@ export default async function AdtPage(props: { params: Params }) {
</div>
<div className="space-y-3">
<button
<ShowNumberModal phoneNumber={String(adt.user?.email)} session={session} />
{/* <button
// onClick={() => setOpenShowNumberModal(true)}
className="w-full flex items-center justify-center gap-2 bg-indigo-600 text-white px-4 py-2 rounded-lg hover:bg-indigo-700"
>
<Phone className="h-5 w-5" />
<span>Show Phone Number</span>
</button>
</button> */}
<button className="w-full flex items-center justify-center gap-2 bg-white border border-indigo-600 text-indigo-600 px-4 py-2 rounded-lg hover:bg-indigo-50">
<MessageCircle className="h-5 w-5" />
<span>Send Message (In development)</span>

View File

@ -2,47 +2,23 @@
import Categories from "@/components/Categories";
import ListingCard from "@/components/ListingCard";
import { BlockAdts } from "@/components/shared/block-adts";
import { getUserSession } from "@/lib/get-user-session";
import { prisma } from "@/prisma/prisma-client";
import toast from "react-hot-toast";
export default async function Home() {
const adts = await prisma.adt.findMany()
const session = await getUserSession()
// if (!session) {
// return redirect('/not-auth')
// }
// const user = await prisma.user.findFirst({
// where: {
// id: Number(session?.id)
// }
// })
// console.log(user)
console.log("session",session)
// const session = await getUserSession()
// // console.log(user)
// console.log("session",session)
return (
<>
<div className="min-h-screen bg-gray-100">
<Categories />
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="flex justify-between items-center mb-6">
<h2 className="text-xl font-semibold">Featured Listings</h2>
<div className="flex gap-2">
<select className="px-4 py-2 rounded-lg border border-gray-200 bg-white">
<option>Most Recent</option>
<option>Price: Low to High</option>
<option>Price: High to Low</option>
</select>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{adts.map((adt) => (
<ListingCard key={adt.id} title={adt.title} image={String(adt.image)} price={String(adt.price)} location={String(adt.location)} date={String(adt.createdAt)} id={String(adt.id)} />
))}
</div>
<BlockAdts />
</main>
</div>
</>

View File

@ -4,6 +4,7 @@ import type { Metadata } from "next";
import localFont from "next/font/local";
import "./globals.css";
import { SessionProvider } from "next-auth/react"
import { Toaster } from 'react-hot-toast';
export default function RootLayout({
@ -16,6 +17,7 @@ export default function RootLayout({
<body>
<SessionProvider>
{children}
<Toaster />
</SessionProvider>
</body>
</html>

View File

@ -6,6 +6,7 @@ import { useSession, signIn } from 'next-auth/react';
import { Button } from './ui/button';
import { ProfileButton } from './shared/profile-button';
import { AuthModal } from './shared/modals/auth-modal/auth-modal';
import AddButton from './shared/add-button';
export default function Header() {
const [openAuthModal, setOpenAuthModal] = React.useState(false)
@ -42,24 +43,15 @@ export default function Header() {
<Search className="h-6 w-6 text-gray-600" />
</button>
<Link
href="/adt/create"
className="p-2 hover:bg-gray-100 rounded-full"
>
<PlusCircle className="h-6 w-6 text-indigo-600" />
</Link>
<AddButton />
<ProfileButton onClickSignIn={() => setOpenAuthModal(true)} />
</div>
{/* Desktop Navigation */}
<div className="hidden sm:flex items-center gap-4">
<Link
href="/adt/create"
className="p-2 hover:bg-gray-100 rounded-full"
>
<PlusCircle className="h-6 w-6 text-indigo-600" />
</Link>
<AddButton />
{/* <button className="relative p-2 hover:bg-gray-100 rounded-full">
<Bell className="h-6 w-6 text-gray-600" />
<span className="absolute top-0 right-0 h-4 w-4 bg-red-500 rounded-full text-xs text-white flex items-center justify-center">

View File

@ -24,7 +24,7 @@ export default function ListingCard({ id, title, price, location, image, date }:
alt={title}
className="w-full h-full object-cover rounded-t-xl"
/>
<button
{/* <button
className="absolute top-3 right-3 p-2 bg-white/90 rounded-full hover:bg-white"
onClick={(e) => {
e.preventDefault();
@ -32,7 +32,7 @@ export default function ListingCard({ id, title, price, location, image, date }:
}}
>
<Heart className="h-5 w-5 text-gray-600" />
</button>
</button> */}
</div>
<div className="p-4">
<div className="flex justify-between items-start mb-2">

View File

@ -0,0 +1,38 @@
'use client';
import { useState } from 'react';
import { PlusCircle } from 'lucide-react';
import Link from 'next/link';
import { useSession } from 'next-auth/react';
import { AuthModal } from './modals/auth-modal/auth-modal';
import toast from 'react-hot-toast';
export default function AddButton() {
const [openAuthModal, setOpenAuthModal] = useState(false);
const { data: session } = useSession();
const handleClick = () => {
if (!session) {
toast.error("Authorization required")
setOpenAuthModal(true);
return;
}
};
return (
<>
<Link
href={session ? "/adt/create" : "#"}
onClick={handleClick}
className="p-2 hover:bg-gray-100 rounded-full"
>
<PlusCircle className="h-6 w-6 text-indigo-600" />
</Link>
<AuthModal
open={openAuthModal}
onClose={() => setOpenAuthModal(false)}
/>
</>
);
}

View File

@ -7,6 +7,7 @@ import { formAdtCreateSchema, type TFormAdtCreateValues } from './schemas';
import Image from 'next/image';
import { Category } from '@prisma/client';
import { useRouter } from 'next/navigation';
import toast from 'react-hot-toast';
interface CreateAdtFormProps {
categories: Category[];
@ -59,7 +60,9 @@ export default function AdtCreateForm({ categories }: CreateAdtFormProps) {
});
if (!response.ok) {
throw new Error('Ошибка при создании объявления');
const err = await response.text()
toast.error("Error creating adt: " + err)
throw new Error('Error creating adt');
}
reset();
@ -67,6 +70,8 @@ export default function AdtCreateForm({ categories }: CreateAdtFormProps) {
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
toast.success("Adt created successfully")
// Здесь можно добавить уведомление об успешном создании
const data_f = await response.json();
// Редирект на страницу созданного объявления
@ -76,6 +81,7 @@ export default function AdtCreateForm({ categories }: CreateAdtFormProps) {
} catch (error) {
console.error('Error:', error);
// toast.error("Error creating adt: " + error)
// Здесь можно добавить обработку ошибок
} finally {
setIsSubmitting(false);

View File

@ -2,10 +2,10 @@ import {z} from 'zod'
export const formAdtCreateSchema = z.object({
title: z.string()
.min(5, 'Заголовок должен содержать минимум 5 символов')
.min(2, 'Заголовок должен содержать минимум 5 символов')
.max(100, 'Заголовок не может быть длиннее 100 символов'),
description: z.string()
.min(20, 'Описание должно содержать минимум 20 символов')
.min(10, 'Описание должно содержать минимум 20 символов')
.max(1000, 'Описание не может быть длиннее 1000 символов')
.nullable(),
price: z.string()

View File

@ -0,0 +1,158 @@
"use client"
// Импортируем необходимые компоненты и типы
import { FC, useEffect, useState, useRef, useCallback } from 'react'
import ListingCard from '../ListingCard'
import { Adt } from '@prisma/client'
import toast from 'react-hot-toast'
interface BlockAdtsProps {}
export const BlockAdts: FC<BlockAdtsProps> = () => {
// Состояния для хранения объявлений и управления их отображением
const [adts, setAdts] = useState<Adt[]>([]) // Массив объявлений
const [sortBy, setSortBy] = useState('new') // Тип сортировки
const [isLoading, setIsLoading] = useState(true) // Флаг загрузки
const [isLoadingMore, setIsLoadingMore] = useState(false) // Флаг загрузки дополнительных объявлений
const [page, setPage] = useState(1) // Текущая страница
const [hasMore, setHasMore] = useState(true) // Флаг наличия дополнительных объявлений
// Создаем наблюдатель для бесконечной прокрутки
const observer = useRef<IntersectionObserver>()
const lastAdtElementRef = useCallback((node: HTMLDivElement) => {
if (isLoadingMore) return
if (observer.current) observer.current.disconnect()
observer.current = new IntersectionObserver(entries => {
// Если последний элемент виден и есть еще объявления - загружаем следующую порцию
if (entries[0].isIntersecting && hasMore) {
loadMore()
}
})
if (node) observer.current.observe(node)
}, [isLoadingMore, hasMore])
// Функция загрузки объявлений
const loadAdts = async (pageNum: number, isLoadMore = false) => {
try {
// Устанавливаем соответствующий флаг загрузки
if (isLoadMore) {
setIsLoadingMore(true)
} else {
setIsLoading(true)
}
// Запрашиваем данные с сервера
const response = await fetch(`/api/adt?page=${pageNum}&sort=${sortBy}`)
const { data: newAdts, meta } = await response.json()
// Обновляем список объявлений
if (pageNum === 1) {
setAdts(newAdts)
} else {
setAdts(prev => [...prev, ...newAdts])
}
// Проверяем, есть ли еще объявления для загрузки
setHasMore(newAdts.length > 0 && pageNum < meta.totalPages)
} catch (error) {
console.error('Ошибка загрузки объявлений:', error)
} finally {
setIsLoading(false)
setIsLoadingMore(false)
}
}
// Загружаем объявления при изменении способа сортировки
useEffect(() => {
loadAdts(1)
}, [sortBy])
// Обработчик изменения сортировки
const handleSort = (event: React.ChangeEvent<HTMLSelectElement>) => {
setSortBy(event.target.value)
setAdts([])
setPage(1)
setHasMore(true)
}
// Функция загрузки дополнительных объявлений
const loadMore = () => {
if (!isLoadingMore && hasMore) {
const nextPage = page + 1
setPage(nextPage)
loadAdts(nextPage, true)
}
}
// Компонент-заглушка для отображения во время загрузки
const SkeletonCard = () => (
<div className="bg-white rounded-lg shadow-md p-4 animate-pulse">
<div className="w-full h-48 bg-gray-200 rounded-lg mb-4"></div>
<div className="h-4 bg-gray-200 rounded w-3/4 mb-2"></div>
<div className="h-4 bg-gray-200 rounded w-1/2 mb-2"></div>
<div className="h-4 bg-gray-200 rounded w-1/4"></div>
</div>
)
return (
<div className="space-y-6">
{/* Заголовок и селектор сортировки */}
<div className="flex justify-between items-center">
<h2 className="text-xl font-semibold">Listings</h2>
<div className="flex gap-2">
<select
className="px-4 py-2 rounded-lg border border-gray-200 bg-white"
onChange={handleSort}
value={sortBy}
>
<option value="new">Newest</option>
<option value="price_asc">Price: ascending</option>
<option value="price_desc">Price: descending</option>
</select>
</div>
</div>
{/* Сетка объявлений */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{isLoading ? (
// Отображаем заглушки во время начальной загрузки
<>
<SkeletonCard />
<SkeletonCard />
<SkeletonCard />
<SkeletonCard />
<SkeletonCard />
<SkeletonCard />
</>
) : (
// Отображаем список объявлений
adts.map((adt, index) => (
<div
key={adt.id}
ref={index === adts.length - 1 ? lastAdtElementRef : undefined}
>
<ListingCard
title={adt.title}
image={String(adt.image)}
price={String(adt.price)}
location={String(adt.location)}
date={String(adt.createdAt)}
id={String(adt.id)}
/>
</div>
))
)}
</div>
{/* Отображаем заглушки при загрузке дополнительных объявлений */}
{isLoadingMore && (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mt-6">
<SkeletonCard />
<SkeletonCard />
<SkeletonCard />
</div>
)}
</div>
)
}

View File

@ -19,6 +19,7 @@ export const AuthModal: React.FC<Props> = ({ open, onClose}) => {
const handleClose = () => {
onClose()
setType('login')
}
return (
@ -31,7 +32,7 @@ export const AuthModal: React.FC<Props> = ({ open, onClose}) => {
<hr />
<div className="flex gap-2">
<Button
{/* <Button
variant='secondary'
onClick={() =>
signIn('github', {
@ -44,9 +45,9 @@ export const AuthModal: React.FC<Props> = ({ open, onClose}) => {
>
<img className="2-6 h-6" src="https://github.githubassets.com/favicons/favicon.svg" />
GitHub
</Button>
</Button> */}
<Button
{/* <Button
variant='secondary'
onClick={() =>
signIn('google', {
@ -59,11 +60,11 @@ export const AuthModal: React.FC<Props> = ({ open, onClose}) => {
>
<img className="2-6 h-6" src="https://fonts.gstatic.com/s/i/productlogos/googleg/v6/24px.svg" />
Google
</Button>
</Button> */}
</div>
<Button variant='outline' onClick={onSwitchType} type='button' className="h-12">
{type === 'login' ? 'Регистрация' : 'Войти'}
{type === 'login' ? 'Sign Up' : 'Login'}
</Button>
</DialogContent>

View File

@ -6,6 +6,7 @@ import { Title } from "@/components/shared/title";
import { FormInput } from "@/components/shared/form";
import { Button } from "@/components/ui/button";
import { signIn } from "next-auth/react";
import toast from "react-hot-toast";
interface Props {
onClose?: VoidFunction;
@ -28,11 +29,13 @@ export const LoginForm: React.FC<Props> = ({onClose}) => {
})
if (!resp?.ok) {
toast.error("Incorrect login or password")
throw Error();
}
onClose?.()
} catch (error) {
toast.error("Error login")
console.error('Error [LOGIN]', error)
}
}
@ -42,8 +45,8 @@ export const LoginForm: React.FC<Props> = ({onClose}) => {
<form className="flex flex-col gap-5" onSubmit={form.handleSubmit(onSubmit)}>
<div className="fles justify-between items-center">
<div className="mr-2">
<Title text="Вход в аккаунт" size='md' className="font-bold" />
<p className="text-gray-400">Введите свою почту, чтобы войти</p>
<Title text="Login" size='md' className="font-bold" />
<p className="text-gray-400">Enter your email to login</p>
</div>
{/* <img src="..." /> */}
</div>
@ -52,7 +55,7 @@ export const LoginForm: React.FC<Props> = ({onClose}) => {
<Button loading={form.formState.isSubmitting} className="h-12 text-base" type='submit' >
{
'Войти'
'Login'
}
</Button>
</form>

View File

@ -7,6 +7,7 @@ import { FormInput } from "@/components/shared/form";
import { Button } from "@/components/ui/button";
import { signIn } from "next-auth/react";
import { registerUser } from "@/app/actions";
import toast from "react-hot-toast";
interface Props {
onClose?: VoidFunction;
@ -35,22 +36,24 @@ export const RegisterForm: React.FC<Props> = ({onClose}) => {
// throw Error();
// }
toast.success("User registered successfully. Login")
onClose?.()
} catch (error) {
console.error('Error [LOGIN]', error)
toast.error("Error register")
console.error('Error [REGISTER]', error)
}
}
return (
<FormProvider {...form}>
<form className="flex flex-col gap-5" onSubmit={form.handleSubmit(onSubmit)}>
{/* <div className="fles justify-between items-center">
<div className="fles justify-between items-center">
<div className="mr-2">
<Title text="Вход в аккаунт" size='md' className="font-bold" />
<p className="text-gray-400">Введите свою почту, чтобы войти</p>
<Title text="Sign Up" size='md' className="font-bold" />
<p className="text-gray-400">Enter your name and email to sign up</p>
</div>
</div> */}
</div>
<FormInput name='name' label='Name' required />
<FormInput name='email' label='E-Mail' required />
<FormInput name='password' label='Password' required />

View File

@ -1,45 +1,46 @@
import { Dialog } from "@/components/ui/dialog";
'use client'
import { Dialog, DialogContent } from "@/components/ui/dialog";
import { getUserSession } from "@/lib/get-user-session";
import React from "react";
import { Phone } from "lucide-react";
interface Props {
open: boolean;
onClose: () => void;
phoneNumber: string;
session: any;
}
export const ShowNumberModal: React.FC<Props> = ({ open, onClose}) => {
// const [ session, setSession ] = React.useState(null)
export const ShowNumberModal: React.FC<Props> = ({ phoneNumber, session }) => {
const [open, setOpen] = React.useState(false);
// const [session, setSession] = React.useState<any>(null);
// React.useEffect(() => {
// const getSession = async () => {
// const userSession = await getUserSession()
// setSession(userSession)
// }
// getSession()
// }, [])
// const session = await getUserSession()
const handleClose = () => {
onClose()
}
// const userSession = await getUserSession();
// setSession(userSession);
// };
// getSession();
// }, []);
return (
<Dialog open={open} onOpenChange={handleClose}>
{/* <DialogContent className="w-[450px] bg-white p-10"> */}
{/* {
// session && (
// <h1 className="text-2xl font-bold mb-4">892348924823</h1>
// )
} */}
<>
<button
onClick={() => setOpen(true)}
className="w-full flex items-center justify-center gap-2 bg-indigo-600 text-white px-4 py-2 rounded-lg hover:bg-indigo-700"
>
<Phone className="h-5 w-5" />
<span>Show Phone Number</span>
</button>
{/* <h1 className="text-2xl font-bold mb-4">892348924823</h1> */}
<hr />
<div className="flex gap-2">
</div>
{/* </DialogContent> */}
</Dialog>
)
}
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="w-[450px] bg-white p-10">
{session ? (
<h1 className="text-2xl font-bold mb-4">{phoneNumber}</h1>
) : (
<h2 className="text-xl text-center">Please login to see the phone number</h2>
)}
</DialogContent>
</Dialog>
</>
);
};

View File

@ -11,6 +11,7 @@ import { Title } from "./title";
import { FormInput } from "./form/form-input";
import { Button } from "../ui/button";
import { updateUserInfo } from "@/app/actions";
import toast from "react-hot-toast";
interface Props {
data: User
@ -35,13 +36,13 @@ export const ProfileForm: React.FC<Props> = ({ data }) => {
password: formData.password,
});
// toast.error('Данные обновлены 📝', {
// icon: '✅',
// });
toast.success('Data updated 📝', {
icon: '✅',
});
} catch (error) {
// return toast.error('Ошибка при обновлении данных', {
// icon: '❌',
// });
return toast.error('Error updating data', {
icon: '❌',
});
console.log('error update', error)
}
};

27
package-lock.json generated
View File

@ -25,6 +25,7 @@
"react": "19.0.0-rc-66855b96-20241106",
"react-dom": "19.0.0-rc-66855b96-20241106",
"react-hook-form": "^7.53.2",
"react-hot-toast": "^2.4.1",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.23.8"
@ -2492,7 +2493,6 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"devOptional": true,
"license": "MIT"
},
"node_modules/damerau-levenshtein": {
@ -3795,6 +3795,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/goober": {
"version": "2.1.16",
"resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz",
"integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==",
"license": "MIT",
"peerDependencies": {
"csstype": "^3.0.10"
}
},
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
@ -5659,6 +5668,22 @@
"react": "^16.8.0 || ^17 || ^18 || ^19"
}
},
"node_modules/react-hot-toast": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz",
"integrity": "sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==",
"license": "MIT",
"dependencies": {
"goober": "^2.1.10"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": ">=16",
"react-dom": ">=16"
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",

View File

@ -33,6 +33,7 @@
"react": "19.0.0-rc-66855b96-20241106",
"react-dom": "19.0.0-rc-66855b96-20241106",
"react-hook-form": "^7.53.2",
"react-hot-toast": "^2.4.1",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.23.8"

View File

@ -97,5 +97,152 @@ export const categories = [
image: "https://images.unsplash.com/photo-1516035069371-29a1b244cc32?auto=format&fit=crop&w=800",
// date: "Posted 7 days ago"
userId: 1
},
{
title: "2020 Tesla Model 3 Long Range",
price: "$41,999",
location: "San Francisco, CA",
image: "https://images.unsplash.com/photo-1560958089-b8a1929cea89?auto=format&fit=crop&w=800",
userId: 1
},
{
title: "Винтажный велосипед",
price: "$450",
location: "Portland, OR",
image: "https://images.unsplash.com/photo-1485965120184-e220f721d03e?auto=format&fit=crop&w=800",
userId: 2
},
{
title: "Профессиональный набор для рисования",
price: "$299",
location: "Seattle, WA",
image: "https://images.unsplash.com/photo-1513364776144-60967b0f800f?auto=format&fit=crop&w=800",
userId: 1
},
{
title: "Кожаный диван",
price: "$1,299",
location: "Austin, TX",
image: "https://images.unsplash.com/photo-1493663284031-b7e3aefcae8e?auto=format&fit=crop&w=800",
userId: 2
},
{
title: "Коллекционные виниловые пластинки",
price: "$599",
location: "Nashville, TN",
image: "https://images.unsplash.com/photo-1539375665275-f9de415ef9ac?auto=format&fit=crop&w=800",
userId: 1
},
{
title: "Беговая дорожка NordicTrack",
price: "$899",
location: "Phoenix, AZ",
image: "https://images.unsplash.com/photo-1540497077202-7c8a3999166f?auto=format&fit=crop&w=800",
userId: 2
},
{
title: "Набор кухонной посуды",
price: "$399",
location: "Boston, MA",
image: "https://images.unsplash.com/photo-1556911220-bff31c812dba?auto=format&fit=crop&w=800",
userId: 1
},
{
title: "Горный велосипед Trek",
price: "$789",
location: "Denver, CO",
image: "https://images.unsplash.com/photo-1576435728678-68d0fbf94e91?auto=format&fit=crop&w=800",
userId: 2
},
{
title: "Игровая приставка PS5",
price: "$499",
location: "Las Vegas, NV",
image: "https://images.unsplash.com/photo-1606144042614-b2417e99c4e3?auto=format&fit=crop&w=800",
userId: 1
},
{
title: "Антикварный комод",
price: "$850",
location: "Charleston, SC",
image: "https://images.unsplash.com/photo-1505693416388-ac5ce068fe85?auto=format&fit=crop&w=800",
userId: 2
},
{
title: "Дрон DJI Mavic Air 2",
price: "$799",
location: "San Diego, CA",
image: "https://images.unsplash.com/photo-1579829366248-204fe8413f31?auto=format&fit=crop&w=800",
userId: 1
},
{
title: "Электрогитара Fender",
price: "$1,199",
location: "Atlanta, GA",
image: "https://images.unsplash.com/photo-1564186763535-ebb21ef5277f?auto=format&fit=crop&w=800",
userId: 2
},
{
title: "Кофемашина Breville",
price: "$599",
location: "Seattle, WA",
image: "https://images.unsplash.com/photo-1517080317843-0b926a6ce350?auto=format&fit=crop&w=800",
userId: 1
},
{
title: "Умные часы Apple Watch",
price: "$349",
location: "Houston, TX",
image: "https://images.unsplash.com/photo-1546868871-7041f2a55e12?auto=format&fit=crop&w=800",
userId: 2
},
{
title: "Винтажная печатная машинка",
price: "$299",
location: "Portland, ME",
image: "https://images.unsplash.com/photo-1558522195-e1201b090344?auto=format&fit=crop&w=800",
userId: 1
},
{
title: "Телескоп Celestron",
price: "$899",
location: "Tucson, AZ",
image: "https://images.unsplash.com/photo-1566004100631-35d015d6a491?auto=format&fit=crop&w=800",
userId: 2
},
{
title: "Набор для йоги",
price: "$89",
location: "Santa Monica, CA",
image: "https://images.unsplash.com/photo-1544367567-0f2fcb009e0b?auto=format&fit=crop&w=800",
userId: 1
},
{
title: "Винный холодильник",
price: "$599",
location: "Napa Valley, CA",
image: "https://images.unsplash.com/photo-1585703900468-13c7a978ad86?auto=format&fit=crop&w=800",
userId: 2
},
{
title: "Электросамокат Xiaomi",
price: "$399",
location: "Chicago, IL",
image: "https://images.unsplash.com/photo-1589999562311-56254d5d4325?auto=format&fit=crop&w=800",
userId: 1
},
{
title: "Коллекционные монеты",
price: "$2,999",
location: "Washington, DC",
image: "https://images.unsplash.com/photo-1566321343730-237ec35e53f3?auto=format&fit=crop&w=800",
userId: 2
},
{
title: "Профессиональный микрофон",
price: "$299",
location: "Nashville, TN",
image: "https://images.unsplash.com/photo-1590602847861-f357a9332bbc?auto=format&fit=crop&w=800",
userId: 1
}
];
];