Compare commits
1 Commits
81c755ec3a
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c529273c38 |
@@ -0,0 +1,67 @@
|
||||
# LocalPro Finder — v2 (Без AI-диагностики)
|
||||
|
||||
Площадка для поиска мастеров рядом с вами. Версия 2 без AI-агента диагностики.
|
||||
|
||||
## 🚀 Быстрый старт
|
||||
|
||||
### Требования
|
||||
- Docker + Docker Compose
|
||||
- PostgreSQL 16+
|
||||
- Redis 7+
|
||||
|
||||
### Запуск через Docker Compose
|
||||
|
||||
```bash
|
||||
# 1. Скопируйте .env.example в .env и заполните переменные
|
||||
cp backend/.env.example backend/.env
|
||||
|
||||
# 2. Запустите стек
|
||||
docker compose up -d --build
|
||||
|
||||
# 3. Проверьте что всё работает
|
||||
curl http://localhost:8000/api/health
|
||||
```
|
||||
|
||||
## 📦 Фичи v2
|
||||
|
||||
- ✅ Регистрация / Вход (JWT)
|
||||
- ✅ Создание проектов (запрос на услугу)
|
||||
- ✅ Назначение мастера на проект
|
||||
- ✅ Система отзывов и рейтингов
|
||||
- ✅ Встроенный чат между клиентом и мастером
|
||||
- ✅ Подписки (Premium для мастеров)
|
||||
- ⏳ AI Диагностика — Coming Soon
|
||||
|
||||
## 📁 Структура
|
||||
|
||||
```
|
||||
localpro-finder-v2/
|
||||
├── backend/ # FastAPI бэкенд
|
||||
│ ├── src/api/routes/
|
||||
│ │ ├── auth.py # Регистрация, логин, JWT
|
||||
│ │ ├── projects.py # Создание проектов
|
||||
│ │ ├── reviews.py # Отзывы и рейтинги
|
||||
│ │ ├── chats.py # Чат между клиентом и мастером
|
||||
│ │ └── subscriptions.py # Подписки мастеров
|
||||
├── frontend/ # Next.js фронтенд
|
||||
│ ├── src/pages/
|
||||
│ │ ├── index.tsx # Главная (поиск, категории)
|
||||
│ │ ├── login.tsx # Вход
|
||||
│ │ └── register.tsx # Регистрация
|
||||
├── docker-compose.yml
|
||||
```
|
||||
|
||||
## 🔧 API Endpoints
|
||||
|
||||
| Метод | Путь | Описание |
|
||||
|-------|------|----------|
|
||||
| POST | `/api/auth/register` | Регистрация пользователя |
|
||||
| POST | `/api/auth/login` | Вход (JWT) |
|
||||
| POST | `/api/projects/` | Создать проект |
|
||||
| POST | `/api/projects/{id}/assign-master` | Назначить мастера |
|
||||
| GET | `/api/reviews/master/{id}` | Отзывы мастера |
|
||||
| POST | `/api/chats/project/{id}/send` | Отправить сообщение |
|
||||
|
||||
## 📝 License
|
||||
|
||||
MIT © 2026 LocalPro Finder
|
||||
@@ -1,77 +0,0 @@
|
||||
"""Онлайн-диагностика — AI задаёт вопросы, мастер подтверждает"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
import uuid
|
||||
|
||||
from ...core.database import async_session_factory, Diagnosis, Project
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class CreateDiagnosisRequest(BaseModel):
|
||||
project_id: uuid.UUID
|
||||
problem_description: str
|
||||
photos: list[str] = []
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def create_diagnosis(req: CreateDiagnosisRequest, session: AsyncSession = Depends(async_session_factory), user=Depends(get_current_user)):
|
||||
"""Создать диагностику проблемы."""
|
||||
|
||||
project = await session.get(Project, req.project_id)
|
||||
if not project or project.client_id != user.id:
|
||||
raise HTTPException(403)
|
||||
|
||||
diagnosis = Diagnosis(
|
||||
project_id=req.project_id,
|
||||
problem_description=req.problem_description,
|
||||
photos=req.photos,
|
||||
ai_questions=["Какой тип крана?", "Как давно течёт?"], # AI генерирует динамически
|
||||
)
|
||||
session.add(diagnosis)
|
||||
await session.commit()
|
||||
|
||||
return {"id": str(diagnosis.id), "ai_questions": diagnosis.ai_questions}
|
||||
|
||||
|
||||
@router.post("/{diagnosis_id}/answer")
|
||||
async def answer_diagnosis(diagnosis_id: uuid.UUID, answers: dict, session: AsyncSession = Depends(async_session_factory)):
|
||||
"""Ответить на вопросы диагностики."""
|
||||
|
||||
diagnosis = await session.get(Diagnosis, diagnosis_id)
|
||||
if not diagnosis:
|
||||
raise HTTPException(404)
|
||||
|
||||
diagnosis.client_answers = answers
|
||||
# AI генерирует результат
|
||||
diagnosis.diagnosis_result = "Замена картриджа однорычажного крана. Стоимость ~1500₽."
|
||||
await session.commit()
|
||||
|
||||
return {"diagnosis_result": diagnosis.diagnosis_result}
|
||||
|
||||
|
||||
@router.post("/{diagnosis_id}/confirm")
|
||||
async def confirm_diagnosis(diagnosis_id: uuid.UUID, master_confirmation: str, session: AsyncSession = Depends(async_session_factory)):
|
||||
"""Мастер подтверждает диагностику."""
|
||||
|
||||
diagnosis = await session.get(Diagnosis, diagnosis_id)
|
||||
if not diagnosis:
|
||||
raise HTTPException(404)
|
||||
|
||||
diagnosis.master_confirmation = master_confirmation
|
||||
await session.commit()
|
||||
|
||||
|
||||
@router.get("/project/{project_id}")
|
||||
async def get_project_diagnosis(project_id: uuid.UUID, session: AsyncSession = Depends(async_session_factory)):
|
||||
"""Получить диагностику проекта."""
|
||||
|
||||
result = await session.execute(select(Diagnosis).where(Diagnosis.project_id == project_id))
|
||||
diagnosis = result.scalar_one_or_none()
|
||||
return diagnosis.mapped() if diagnosis else None
|
||||
|
||||
|
||||
from sqlalchemy import select
|
||||
+2
-3
@@ -6,7 +6,7 @@ services:
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-postgres}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
|
||||
POSTGRES_DB: freelancer_match
|
||||
POSTGRES_DB: localpro_finder
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
@@ -30,10 +30,9 @@ services:
|
||||
backend:
|
||||
build: ./backend
|
||||
environment:
|
||||
DATABASE_URL: postgresql+asyncpg://postgres:${POSTGRES_PASSWORD:-postgres}@postgres:5432/freelancer_match
|
||||
DATABASE_URL: postgresql+asyncpg://postgres:${POSTGRES_PASSWORD:-postgres}@postgres:5432/localpro_finder
|
||||
REDIS_URL: redis://redis:6379/0
|
||||
SECRET_KEY: ${SECRET_KEY}
|
||||
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
||||
ports:
|
||||
- "8000:8000"
|
||||
depends_on:
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { useState } from "react";
|
||||
import { Search, MapPin, Star, MessageCircle, ShieldCheck, Clock, ChevronRight } from "lucide-react";
|
||||
|
||||
export default function Home() {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [location, setLocation] = useState("");
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-blue-50 to-white">
|
||||
{/* Hero */}
|
||||
<section className="relative overflow-hidden bg-gradient-to-r from-blue-600 to-indigo-700 text-white py-24 px-8">
|
||||
<div className="max-w-4xl mx-auto text-center">
|
||||
<h1 className="text-5xl font-bold mb-4">LocalPro Finder</h1>
|
||||
<p className="text-xl opacity-90 mb-8">Найдите проверенных мастеров рядом с вами</p>
|
||||
|
||||
{/* Search */}
|
||||
<div className="flex gap-3 max-w-2xl mx-auto">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Что вам нужно?"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="flex-1 px-4 py-3 rounded-lg text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-300"
|
||||
/>
|
||||
<button className="px-6 py-3 bg-white text-blue-700 font-semibold rounded-lg hover:bg-blue-50 transition">
|
||||
Найти
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Location */}
|
||||
<div className="flex items-center justify-center gap-2 mt-4 text-sm opacity-80">
|
||||
<MapPin size={16} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Ваш город или адрес"
|
||||
value={location}
|
||||
onChange={(e) => setLocation(e.target.value)}
|
||||
className="bg-transparent border-none outline-none text-white placeholder-blue-200 w-full max-w-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Categories */}
|
||||
<section className="py-16 px-8">
|
||||
<h2 className="text-3xl font-bold text-center mb-12">Категории мастеров</h2>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6 max-w-5xl mx-auto">
|
||||
{[
|
||||
{ icon: "🔧", name: "Сантехника" },
|
||||
{ icon: "⚡", name: "Электрика" },
|
||||
{ icon: "🏠", name: "Ремонт" },
|
||||
{ icon: "🎨", name: "Дизайн" },
|
||||
{ icon: "🌿", name: "Ландшафт" },
|
||||
{ icon: "🔑", name: "Замок." },
|
||||
].map((cat) => (
|
||||
<div key={cat.name} className="bg-white rounded-xl p-6 shadow-sm hover:shadow-md transition cursor-pointer text-center">
|
||||
<span className="text-4xl">{cat.icon}</span>
|
||||
<p className="mt-3 font-medium">{cat.name}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* AI Diagnosis — Coming Soon */}
|
||||
<section className="py-16 px-8 bg-indigo-50">
|
||||
<div className="max-w-4xl mx-auto text-center">
|
||||
<ShieldCheck size={48} className="mx-auto mb-4 text-indigo-600" />
|
||||
<h2 className="text-3xl font-bold mb-2">AI Диагностика</h2>
|
||||
<p className="text-lg opacity-75 mb-6">Умная диагностика проблемы — скоро!</p>
|
||||
|
||||
<div className="inline-block bg-white rounded-xl p-8 shadow-sm border border-indigo-100 max-w-md">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<Clock size={20} className="text-indigo-500" />
|
||||
<span className="font-semibold text-indigo-700">Coming Soon</span>
|
||||
</div>
|
||||
<p className="text-sm opacity-60">
|
||||
AI задаст вопросы о вашей проблеме и поможет мастеру точнее оценить стоимость работ.
|
||||
Функция в разработке — следите за обновлениями!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* How it works */}
|
||||
<section className="py-16 px-8">
|
||||
<h2 className="text-3xl font-bold text-center mb-12">Как это работает</h2>
|
||||
<div className="grid md:grid-cols-4 gap-8 max-w-5xl mx-auto">
|
||||
{[
|
||||
{ step: "1", title: "Опишите задачу", desc: "Расскажите что нужно сделать" },
|
||||
{ step: "2", title: "Найдите мастера", desc: "Сравните отзывы и цены" },
|
||||
{ step: "3", title: "Обсудите детали", desc: "Чат с мастером в приложении" },
|
||||
{ step: "4", title: "Оплатите безопасно", desc: "Escrow-гарант сделки" },
|
||||
].map((item) => (
|
||||
<div key={item.step} className="text-center">
|
||||
<div className="w-12 h-12 rounded-full bg-blue-600 text-white flex items-center justify-center mx-auto mb-4 font-bold">{item.step}</div>
|
||||
<h3 className="font-semibold mb-2">{item.title}</h3>
|
||||
<p className="text-sm opacity-75">{item.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="py-8 text-center text-sm opacity-60">
|
||||
© 2026 LocalPro Finder. Все права защищены.
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { useState } from "react";
|
||||
import { Mail, Lock, Eye, EyeOff } from "lucide-react";
|
||||
|
||||
export default function Login() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-blue-50 to-white flex items-center justify-center px-4">
|
||||
<div className="bg-white rounded-xl shadow-lg p-8 w-full max-w-md">
|
||||
<h1 className="text-2xl font-bold text-center mb-6">Вход в LocalPro Finder</h1>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="your@email.com"
|
||||
className="w-full px-4 py-3 rounded-lg border focus:outline-none focus:ring-2 focus:ring-blue-300"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Пароль</label>
|
||||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
className="w-full px-4 py-3 rounded-lg border focus:outline-none focus:ring-2 focus:ring-blue-300"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button className="w-full py-3 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 transition">
|
||||
Войти
|
||||
</button>
|
||||
|
||||
<p className="text-center text-sm opacity-60">
|
||||
Нет аккаунта?{" "}
|
||||
<a href="/register" className="text-blue-600 hover:underline">Зарегистрироваться</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useState } from "react";
|
||||
import { Mail, Lock, User, Phone } from "lucide-react";
|
||||
|
||||
export default function Register() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [firstName, setFirstName] = useState("");
|
||||
const [lastName, setLastName] = useState("");
|
||||
const [phone, setPhone] = useState("");
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-blue-50 to-white flex items-center justify-center px-4 py-12">
|
||||
<div className="bg-white rounded-xl shadow-lg p-8 w-full max-w-md">
|
||||
<h1 className="text-2xl font-bold text-center mb-6">Регистрация</h1>
|
||||
|
||||
<div className="space-y-4">
|
||||
<input value={firstName} onChange={(e) => setFirstName(e.target.value)} placeholder="Имя" className="w-full px-4 py-3 rounded-lg border focus:outline-none focus:ring-2 focus:ring-blue-300" />
|
||||
<input value={lastName} onChange={(e) => setLastName(e.target.value)} placeholder="Фамилия" className="w-full px-4 py-3 rounded-lg border focus:outline-none focus:ring-2 focus:ring-blue-300" />
|
||||
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" className="w-full px-4 py-3 rounded-lg border focus:outline-none focus:ring-2 focus:ring-blue-300" />
|
||||
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Пароль" className="w-full px-4 py-3 rounded-lg border focus:outline-none focus:ring-2 focus:ring-blue-300" />
|
||||
<input type="tel" value={phone} onChange={(e) => setPhone(e.target.value)} placeholder="Телефон (необязательно)" className="w-full px-4 py-3 rounded-lg border focus:outline-none focus:ring-2 focus:ring-blue-300" />
|
||||
|
||||
<button className="w-full py-3 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 transition">
|
||||
Создать аккаунт
|
||||
</button>
|
||||
|
||||
<p className="text-center text-sm opacity-60">
|
||||
Уже есть аккаунт?{" "}
|
||||
<a href="/login" className="text-blue-600 hover:underline">Войти</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user