init commit
This commit is contained in:
74
templates/base.html
Normal file
74
templates/base.html
Normal file
@@ -0,0 +1,74 @@
|
||||
{% load static %}
|
||||
<!doctype html>
|
||||
<html lang="ru" class="h-full">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}Брачное агентство — MatchAgency{% endblock %}</title>
|
||||
<link rel="stylesheet" href="{% static 'style.css' %}">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
|
||||
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
</head>
|
||||
<body class="min-h-full bg-gradient-to-br from-sky-50 to-rose-50">
|
||||
<header class="bg-white/80 backdrop-blur border-b">
|
||||
<nav class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-3 flex items-center justify-between">
|
||||
<a href="/" class="font-semibold text-xl tracking-tight">💍 MatchAgency</a>
|
||||
<div class="flex items-center gap-4">
|
||||
<a href="/profiles/" class="text-sm font-medium hover:underline">Профили</a>
|
||||
{% if api_user %}
|
||||
<span class="text-sm text-gray-600">Здравствуйте, {{ api_user.name|default:api_user.email }}</span>
|
||||
<form action="/logout/" method="post">
|
||||
{% csrf_token %}
|
||||
<button class="text-sm text-white bg-gray-800 hover:bg-black rounded-md px-3 py-1.5">Выйти</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<a href="/login/" class="text-sm text-white bg-indigo-600 hover:bg-indigo-700 rounded-md px-3 py-1.5">Войти</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-6">
|
||||
{% if messages %}
|
||||
<div class="space-y-2 mb-4">
|
||||
{% for m in messages %}
|
||||
<div class="rounded-md border px-3 py-2 {% if m.tags == 'error' %}bg-rose-50 border-rose-200 text-rose-900{% elif m.tags == 'success' %}bg-emerald-50 border-emerald-200 text-emerald-900{% else %}bg-white/70{% endif %}">
|
||||
{{ m }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<footer class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-10 text-sm text-gray-500">
|
||||
<div class="border-t pt-6 flex items-center justify-between">
|
||||
<p>© {{ now|default:2025 }} MatchAgency. Все права защищены.</p>
|
||||
<p><a href="/profiles/" class="hover:underline">Каталог</a> · <a href="/login/" class="hover:underline">Войти</a></p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// CSRF для htmx через cookie
|
||||
function getCookie(name) {
|
||||
let cookieValue = null;
|
||||
if (document.cookie && document.cookie !== '') {
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
const cookie = cookies[i].trim();
|
||||
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cookieValue;
|
||||
}
|
||||
document.body.addEventListener('htmx:configRequest', function (evt) {
|
||||
const token = getCookie('csrftoken');
|
||||
if (token) evt.detail.headers['X-CSRFToken'] = token;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
192
templates/ui/cabinet.html
Normal file
192
templates/ui/cabinet.html
Normal file
@@ -0,0 +1,192 @@
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Кабинет</title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<link href="{% static 'style.css' %}" rel="stylesheet">
|
||||
<style>
|
||||
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, "Helvetica Neue", Arial, "Noto Sans", sans-serif; margin:0; background:#f7f7fb; color:#111; }
|
||||
.topbar { display:flex; gap:16px; align-items:center; padding:14px 18px; background:#111827; color:#fff; }
|
||||
.topbar a { color:#cfe3ff; text-decoration:none; }
|
||||
.container { max-width:1100px; margin:24px auto; padding:0 16px; }
|
||||
.heading { display:flex; align-items:flex-end; gap:12px; margin:8px 0 18px; }
|
||||
.heading h1 { margin:0; font-size:24px; }
|
||||
.muted { color:#6b7280; font-size:14px; }
|
||||
.card { background:#fff; border:1px solid #e5e7eb; border-radius:12px; padding:18px; box-shadow: 0 1px 2px rgba(0,0,0,.03); }
|
||||
.grid { display:grid; gap:16px; }
|
||||
.grid-2 { grid-template-columns: 1fr 1fr; }
|
||||
.form { display:grid; gap:14px; }
|
||||
.form label { font-weight:600; font-size:14px; }
|
||||
.form input[type="text"], .form select, .form textarea {
|
||||
width:100%; border:1px solid #d1d5db; border-radius:8px; padding:10px 12px; font:inherit; background:#fff;
|
||||
}
|
||||
.form small { color:#6b7280; }
|
||||
.btnrow { display:flex; gap:10px; margin-top:8px; }
|
||||
.btn { display:inline-block; padding:10px 14px; border-radius:10px; border:1px solid transparent; font-weight:600; cursor:pointer; }
|
||||
.btn-primary { background:#2563eb; color:#fff; }
|
||||
.btn-outline { background:#fff; color:#111; border-color:#d1d5db; }
|
||||
.messages { list-style:none; padding:0; margin:0 0 16px; }
|
||||
.messages li { padding:10px 12px; margin-bottom:8px; border-radius:10px; }
|
||||
.messages li.success { background:#ecfdf5; color:#065f46; border:1px solid #a7f3d0; }
|
||||
.messages li.error { background:#fef2f2; color:#991b1b; border:1px solid #fecaca; }
|
||||
.messages li.info { background:#eff6ff; color:#1e40af; border:1px solid #bfdbfe; }
|
||||
dl { display:grid; grid-template-columns: 200px 1fr; gap:8px 14px; margin: 0; }
|
||||
dt { font-weight:600; color:#374151; }
|
||||
dd { margin:0; color:#111827; }
|
||||
.pill { display:inline-block; padding:4px 10px; border-radius:999px; background:#eef2ff; color:#3730a3; font-size:12px; margin:2px 6px 2px 0; }
|
||||
details summary { cursor:pointer; }
|
||||
code, pre { background:#111827; color:#e5e7eb; padding:10px 12px; border-radius:10px; display:block; overflow:auto; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="topbar">
|
||||
<div style="flex:1 1 auto;">
|
||||
{% with header_name=header_name|default:request.session.user_full_name|default:request.session.user_email %}
|
||||
Здравствуйте, <strong>{{ header_name }}</strong>!
|
||||
{% endwith %}
|
||||
</div>
|
||||
<nav style="display:flex; gap:14px;">
|
||||
<a href="{% url 'index' %}">Главная</a>
|
||||
<a href="{% url 'cabinet' %}">Кабинет</a>
|
||||
<a href="{% url 'profiles' %}">Каталог</a>
|
||||
<a href="{% url 'logout' %}">Выход</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
|
||||
{% if messages %}
|
||||
<ul class="messages">
|
||||
{% for message in messages %}
|
||||
<li class="{{ message.tags }}">{{ message }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<div class="heading">
|
||||
<h1>Кабинет</h1>
|
||||
{% if has_profile %}
|
||||
<span class="muted">профиль создан</span>
|
||||
{% else %}
|
||||
<span class="muted">профиль ещё не создан</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Две колонки: Аккаунт + Профиль -->
|
||||
<div class="grid grid-2">
|
||||
<!-- ======= ДАННЫЕ АККАУНТА ======= -->
|
||||
<section class="card">
|
||||
<h2 class="muted" style="margin-top:0;">Данные аккаунта</h2>
|
||||
<dl>
|
||||
<dt>Имя</dt>
|
||||
<dd>{{ request.session.user_full_name|default:"—" }}</dd>
|
||||
|
||||
<dt>Email</dt>
|
||||
<dd>{{ request.session.user_email|default:"—" }}</dd>
|
||||
|
||||
<dt>Роль</dt>
|
||||
<dd><span class="pill">{{ request.session.user_role|default:"—" }}</span></dd>
|
||||
|
||||
<dt>ID пользователя</dt>
|
||||
<dd><code>{{ request.session.user_id|default:"—" }}</code></dd>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<!-- ======= ДАННЫЕ ПРОФИЛЯ ======= -->
|
||||
<section class="card">
|
||||
<h2 class="muted" style="margin-top:0;">Данные профиля</h2>
|
||||
|
||||
{% if has_profile and profile %}
|
||||
<dl>
|
||||
<dt>Пол</dt>
|
||||
<dd>{{ profile.gender|default:"—" }}</dd>
|
||||
|
||||
<dt>Город</dt>
|
||||
<dd>{{ profile.city|default:"—" }}</dd>
|
||||
|
||||
<dt>Языки</dt>
|
||||
<dd>
|
||||
{% if profile.languages %}
|
||||
{% for lang in profile.languages %}<span class="pill">{{ lang }}</span>{% endfor %}
|
||||
{% else %} — {% endif %}
|
||||
</dd>
|
||||
|
||||
<dt>Интересы</dt>
|
||||
<dd>
|
||||
{% if profile.interests %}
|
||||
{% for it in profile.interests %}<span class="pill">{{ it }}</span>{% endfor %}
|
||||
{% else %} — {% endif %}
|
||||
</dd>
|
||||
|
||||
<dt>ID профиля</dt>
|
||||
<dd><code>{{ profile.id }}</code></dd>
|
||||
|
||||
<dt>ID пользователя (в профиле)</dt>
|
||||
<dd><code>{{ profile.user_id }}</code></dd>
|
||||
</dl>
|
||||
|
||||
<details style="margin-top:12px;">
|
||||
<summary class="muted">Показать сырой JSON профиля</summary>
|
||||
<pre>{{ profile|safe }}</pre>
|
||||
</details>
|
||||
|
||||
{% else %}
|
||||
<p class="muted">Профиль ещё не создан. Заполните форму ниже.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{% if not has_profile or not profile %}
|
||||
<!-- ======= ФОРМА СОЗДАНИЯ ПРОФИЛЯ ======= -->
|
||||
<section class="card" style="margin-top:16px;" aria-labelledby="section-create">
|
||||
<h2 id="section-create" class="muted" style="margin-top:0;">Создать профиль</h2>
|
||||
|
||||
<form class="form" method="post" action="{% url 'cabinet' %}">
|
||||
{% csrf_token %}
|
||||
<div class="grid grid-2">
|
||||
<div>
|
||||
<label for="gender">Пол</label>
|
||||
<select id="gender" name="gender" required>
|
||||
<option value="">— выберите —</option>
|
||||
<option value="male" {% if request.POST.gender == "male" %}selected{% endif %}>Мужской</option>
|
||||
<option value="female" {% if request.POST.gender == "female" %}selected{% endif %}>Женский</option>
|
||||
<option value="other" {% if request.POST.gender == "other" %}selected{% endif %}>Другое</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="city">Город</label>
|
||||
<input id="city" name="city" type="text" required
|
||||
value="{{ request.POST.city|default_if_none:'' }}" placeholder="Москва">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="languages">Языки</label>
|
||||
<input id="languages" name="languages" type="text"
|
||||
value="{{ request.POST.languages|default_if_none:'' }}" placeholder="ru,en">
|
||||
<small>Несколько — через запятую: <code>ru,en</code></small>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="interests">Интересы</label>
|
||||
<input id="interests" name="interests" type="text"
|
||||
value="{{ request.POST.interests|default_if_none:'' }}" placeholder="music,travel">
|
||||
<small>Несколько — через запятую: <code>music,travel</code></small>
|
||||
</div>
|
||||
|
||||
<div class="btnrow">
|
||||
<button class="btn btn-primary" type="submit">Создать профиль</button>
|
||||
<a class="btn btn-outline" href="{% url 'cabinet' %}">Сбросить</a>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
</main>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
17
templates/ui/components/like_button.html
Normal file
17
templates/ui/components/like_button.html
Normal file
@@ -0,0 +1,17 @@
|
||||
{# expects: profile_id, liked: bool #}
|
||||
<form method="post"
|
||||
hx-post="{% url 'like_profile' profile_id %}"
|
||||
hx-target="this"
|
||||
hx-swap="outerHTML"
|
||||
aria-label="Добавить в избранное">
|
||||
{% csrf_token %}
|
||||
{% if liked %}
|
||||
<button type="submit" class="inline-flex items-center gap-1 rounded-md border px-3 py-1.5 text-sm hover:bg-white">
|
||||
<span aria-hidden="true">❤️</span> В избранном
|
||||
</button>
|
||||
{% else %}
|
||||
<button type="submit" class="inline-flex items-center gap-1 rounded-md border px-3 py-1.5 text-sm hover:bg-white">
|
||||
<span aria-hidden="true">🤍</span> В избранное
|
||||
</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
3
templates/ui/components/like_button_login_required.html
Normal file
3
templates/ui/components/like_button_login_required.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<a href="/login/?next={{ request.path }}" class="inline-flex items-center gap-1 rounded-md bg-indigo-600 text-white px-3 py-1.5 text-sm hover:bg-indigo-700">
|
||||
Войти, чтобы добавить
|
||||
</a>
|
||||
27
templates/ui/components/profile_card.html
Normal file
27
templates/ui/components/profile_card.html
Normal file
@@ -0,0 +1,27 @@
|
||||
{# expects: profile, liked_ids (optional) #}
|
||||
{% with liked = (profile.liked|default_if_none:False) or (liked_ids and profile.id in liked_ids) %}
|
||||
<article class="rounded-xl bg-white/80 backdrop-blur border shadow-sm hover:shadow transition overflow-hidden flex flex-col">
|
||||
<img src="{{ profile.photo }}" alt="Фото {{ profile.name }}" class="w-full h-56 object-cover">
|
||||
<div class="p-4 flex-1 flex flex-col">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<h3 class="text-lg font-semibold">{{ profile.name }}, {{ profile.age }}</h3>
|
||||
{% if profile.verified %}
|
||||
<span class="inline-flex items-center text-[10px] rounded-full bg-emerald-100 text-emerald-800 px-2 py-0.5">проверено</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if profile.city %}<p class="text-sm text-gray-600 mt-1">{{ profile.city }}</p>{% endif %}
|
||||
{% if profile.about %}<p class="text-sm mt-3 line-clamp-2">{{ profile.about }}</p>{% endif %}
|
||||
{% if profile.interests %}
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
{% for tag in profile.interests %}
|
||||
<span class="inline-flex items-center text-xs rounded-full bg-gray-100 px-2 py-1">{{ tag }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<a class="text-sm font-medium text-indigo-700 hover:underline" href="/profiles/{{ profile.id }}/">Подробнее</a>
|
||||
{% include 'ui/components/like_button.html' with profile_id=profile.id liked=liked %}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{% endwith %}
|
||||
31
templates/ui/index.html
Normal file
31
templates/ui/index.html
Normal file
@@ -0,0 +1,31 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Главная — MatchAgency{% endblock %}
|
||||
{% block content %}
|
||||
<section class="grid md:grid-cols-2 gap-8 items-center">
|
||||
<div>
|
||||
<h1 class="text-3xl md:text-5xl font-semibold leading-tight">Подбор идеальных пар под ключ</h1>
|
||||
<p class="mt-4 text-gray-600 text-lg">Фронтенд полностью на API: ни одной локальной таблицы.</p>
|
||||
<div class="mt-6 flex gap-3">
|
||||
<a href="/profiles/" class="inline-flex items-center rounded-md bg-rose-600 px-4 py-2 text-white font-medium hover:bg-rose-700">Смотреть анкеты</a>
|
||||
{% if not api_user %}
|
||||
<a href="/login/" class="inline-flex items-center rounded-md border px-4 py-2 font-medium hover:bg-white">Войти</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-xl bg-white/70 backdrop-blur p-4 md:p-6 shadow">
|
||||
<form action="/profiles/" method="get" class="grid sm:grid-cols-2 gap-4">
|
||||
<input name="q" placeholder="Ключевые слова (хобби, имя, город)"
|
||||
class="w-full rounded-md border px-3 py-2" />
|
||||
<select name="gender" class="w-full rounded-md border px-3 py-2">
|
||||
<option value="">Пол (любой)</option>
|
||||
<option value="female">Женщины</option>
|
||||
<option value="male">Мужчины</option>
|
||||
</select>
|
||||
<input name="age_min" type="number" min="18" max="100" placeholder="От, лет" class="w-full rounded-md border px-3 py-2"/>
|
||||
<input name="age_max" type="number" min="18" max="100" placeholder="До, лет" class="w-full rounded-md border px-3 py-2"/>
|
||||
<input name="city" placeholder="Город" class="w-full rounded-md border px-3 py-2"/>
|
||||
<button class="sm:col-span-2 rounded-md bg-indigo-600 text-white px-4 py-2 hover:bg-indigo-700">Найти</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
27
templates/ui/login.html
Normal file
27
templates/ui/login.html
Normal file
@@ -0,0 +1,27 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Вход — MatchAgency{% endblock %}
|
||||
{% block content %}
|
||||
<div class="max-w-md mx-auto rounded-xl bg-white/80 backdrop-blur border shadow p-6">
|
||||
<h1 class="text-xl font-semibold mb-4">Вход</h1>
|
||||
<form action="" method="post" class="space-y-3">
|
||||
{% csrf_token %}
|
||||
<div>
|
||||
<label class="block text-sm mb-1">Email</label>
|
||||
<input type="email" name="email" required class="w-full rounded-md border px-3 py-2"/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm mb-1">Пароль</label>
|
||||
<input type="password" name="password" required class="w-full rounded-md border px-3 py-2"/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="block text-sm mb-1">Запомнить меня</label>
|
||||
<input type="checkbox" name="remember_me" class="rounded border px-2 py-1"/>
|
||||
</div>
|
||||
{% if error_message %}
|
||||
<p class="text-red-500 text-sm">{{ error_message }}</p>
|
||||
{% endif %}
|
||||
<p class="text-sm">Нет аккаунта? <a class="text-indigo-700 hover:underline" href="{% url 'register' %}">Зарегистрироваться</a></p>
|
||||
<button class="w-full rounded-md bg-indigo-600 text-white px-4 py-2 hover:bg-indigo-700">Войти</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
84
templates/ui/profile_detail.html
Normal file
84
templates/ui/profile_detail.html
Normal file
@@ -0,0 +1,84 @@
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Анкета пользователя</title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<link href="{% static 'style.css' %}" rel="stylesheet">
|
||||
<style>
|
||||
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, "Helvetica Neue", Arial, "Noto Sans", sans-serif; margin:0; background:#f7f7fb; color:#111; }
|
||||
.topbar { display:flex; gap:16px; align-items:center; padding:14px 18px; background:#111827; color:#fff; }
|
||||
.topbar a { color:#cfe3ff; text-decoration:none; }
|
||||
.container { max-width:900px; margin:24px auto; padding:0 16px; }
|
||||
.card { background:#fff; border:1px solid #e5e7eb; border-radius:12px; padding:18px; }
|
||||
.row { display:flex; gap:18px; align-items:center; }
|
||||
.grow { flex:1 1 auto; }
|
||||
.muted { color:#6b7280; font-size:14px; }
|
||||
.pill { display:inline-block; padding:4px 10px; border-radius:999px; background:#eef2ff; color:#3730a3; font-size:12px; margin:2px 6px 2px 0; }
|
||||
.avatar { width:96px; height:96px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-weight:700; font-size:32px; background:#e5e7eb; color:#374151; }
|
||||
.avatar img { width:96px; height:96px; object-fit:cover; border-radius:50%; display:block; }
|
||||
dl { display:grid; grid-template-columns: 220px 1fr; gap:8px 14px; margin:0; }
|
||||
dt { font-weight:600; color:#374151; }
|
||||
dd { margin:0; color:#111827; }
|
||||
.btn { display:inline-block; padding:9px 12px; border-radius:10px; border:1px solid #d1d5db; background:#fff; cursor:pointer; font-weight:600; text-decoration:none; color:#111; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="topbar">
|
||||
<div style="flex:1 1 auto;">Карточка пользователя (ADMIN)</div>
|
||||
<nav style="display:flex; gap:14px;">
|
||||
<a href="{% url 'profiles' %}">← Каталог</a>
|
||||
<a href="{% url 'cabinet' %}">Кабинет</a>
|
||||
<a href="{% url 'logout' %}">Выход</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="avatar">
|
||||
{% if profile.photo %}
|
||||
<img src="{{ profile.photo }}" alt="">
|
||||
{% else %}
|
||||
{{ profile.name|first|upper }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="grow">
|
||||
<div style="font-weight:700; font-size:20px;">{{ profile.name }}</div>
|
||||
|
||||
</div>
|
||||
<div>
|
||||
<form method="post" action="{% url 'like_profile' profile.id %}">
|
||||
{% csrf_token %}
|
||||
{% include "ui/components/like_button.html" with profile_id=profile.id liked=liked %}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top:16px;">
|
||||
<h2 class="muted" style="margin-top:0;">Профиль</h2>
|
||||
<dl>
|
||||
<dt>Пол</dt><dd>{{ profile.gender|default:"—" }}</dd>
|
||||
<dt>Город</dt><dd>{{ profile.city|default:"—" }}</dd>
|
||||
<dt>Языки</dt>
|
||||
<dd>
|
||||
{% if profile.languages %}
|
||||
{% for lang in profile.languages %}<span class="pill">{{ lang }}</span>{% endfor %}
|
||||
{% else %} — {% endif %}
|
||||
</dd>
|
||||
<dt>Интересы</dt>
|
||||
<dd>
|
||||
{% if profile.interests %}
|
||||
{% for it in profile.interests %}<span class="pill">{{ it }}</span>{% endfor %}
|
||||
{% else %} — {% endif %}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
195
templates/ui/profiles_list.html
Normal file
195
templates/ui/profiles_list.html
Normal file
@@ -0,0 +1,195 @@
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Каталог анкет</title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<link href="{% static 'style.css' %}" rel="stylesheet">
|
||||
<style>
|
||||
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, "Helvetica Neue", Arial, "Noto Sans", sans-serif; margin:0; background:#f7f7fb; color:#111; }
|
||||
.topbar { display:flex; gap:16px; align-items:center; padding:14px 18px; background:#111827; color:#fff; }
|
||||
.topbar a { color:#cfe3ff; text-decoration:none; }
|
||||
.container { max-width:1100px; margin:24px auto; padding:0 16px; }
|
||||
.messages { list-style:none; padding:0; margin:0 0 16px; }
|
||||
.messages li { padding:10px 12px; margin-bottom:8px; border-radius:10px; }
|
||||
.messages li.success { background:#ecfdf5; color:#065f46; border:1px solid #a7f3d0; }
|
||||
.messages li.error { background:#fef2f2; color:#991b1b; border:1px solid #fecaca; }
|
||||
.messages li.info { background:#eff6ff; color:#1e40af; border:1px solid #bfdbfe; }
|
||||
|
||||
.filters { display:grid; grid-template-columns: repeat(8, 1fr); gap:10px; background:#fff; border:1px solid #e5e7eb; border-radius:12px; padding:14px; }
|
||||
.filters .full { grid-column: 1 / -1; }
|
||||
.filters input[type="text"], .filters select {
|
||||
width:100%; border:1px solid #d1d5db; border-radius:8px; padding:8px 10px; font:inherit; background:#fff;
|
||||
}
|
||||
.btn { display:inline-block; padding:9px 12px; border-radius:10px; border:1px solid #d1d5db; background:#fff; cursor:pointer; font-weight:600; }
|
||||
.btn-primary { background:#2563eb; color:#fff; border-color:#2563eb; }
|
||||
|
||||
.list { margin-top:16px; display:grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap:14px; }
|
||||
.card { background:#fff; border:1px solid #e5e7eb; border-radius:12px; padding:14px; }
|
||||
.row { display:flex; align-items:center; gap:12px; }
|
||||
.grow { flex:1 1 auto; }
|
||||
.muted { color:#6b7280; font-size:14px; }
|
||||
.pill { display:inline-block; padding:4px 10px; border-radius:999px; background:#eef2ff; color:#3730a3; font-size:12px; margin:2px 6px 2px 0; }
|
||||
|
||||
.meta { margin-top:8px; display:flex; flex-wrap:wrap; gap:10px 18px; }
|
||||
.meta .k { color:#6b7280; }
|
||||
.meta .v { color:#111827; font-weight:600; }
|
||||
|
||||
.pagination { display:flex; gap:10px; margin:16px 0; align-items:center; }
|
||||
.pagination a, .pagination span {
|
||||
padding:8px 12px; border-radius:10px; border:1px solid #d1d5db; text-decoration:none; color:#111;
|
||||
background:#fff;
|
||||
}
|
||||
.pagination .disabled { opacity:.5; pointer-events:none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="topbar">
|
||||
<div style="flex:1 1 auto;">
|
||||
Каталог анкет (ADMIN)
|
||||
</div>
|
||||
<nav style="display:flex; gap:14px;">
|
||||
<a href="{% url 'index' %}">Главная</a>
|
||||
<a href="{% url 'cabinet' %}">Кабинет</a>
|
||||
<a href="{% url 'profiles' %}">Каталог</a>
|
||||
<a href="{% url 'logout' %}">Выход</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
|
||||
{% if messages %}
|
||||
<ul class="messages">
|
||||
{% for message in messages %}
|
||||
<li class="{{ message.tags }}">{{ message }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<form class="filters" method="get" action="{% url 'profiles' %}">
|
||||
<div>
|
||||
<label class="muted">Поиск</label>
|
||||
<input type="text" name="q" value="{{ filters.q }}" placeholder="имя или email">
|
||||
</div>
|
||||
<div>
|
||||
<label class="muted">Роль</label>
|
||||
<select name="role">
|
||||
<option value="">Любая</option>
|
||||
<option value="CLIENT" {% if filters.role == "CLIENT" %}selected{% endif %}>CLIENT</option>
|
||||
<option value="ADMIN" {% if filters.role == "ADMIN" %}selected{% endif %}>ADMIN</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="muted">Активность</label>
|
||||
<select name="active">
|
||||
<option value="">Любая</option>
|
||||
<option value="1" {% if filters.active == "1" %}selected{% endif %}>Активные</option>
|
||||
<option value="0" {% if filters.active == "0" %}selected{% endif %}>Неактивные</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="muted">Домен</label>
|
||||
<input type="text" name="domain" value="{{ filters.domain }}" placeholder="example.com">
|
||||
</div>
|
||||
<div>
|
||||
<label class="muted">Сортировка</label>
|
||||
<select name="sort">
|
||||
<option value="name" {% if filters.sort == "name" %}selected{% endif %}>Имя ↑</option>
|
||||
<option value="name_desc" {% if filters.sort == "name_desc" %}selected{% endif %}>Имя ↓</option>
|
||||
<option value="email" {% if filters.sort == "email" %}selected{% endif %}>Email ↑</option>
|
||||
<option value="email_desc" {% if filters.sort == "email_desc" %}selected{% endif %}>Email ↓</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="muted">На странице</label>
|
||||
<select name="limit">
|
||||
{% for n in page_sizes %}
|
||||
<option value="{{ n }}"{% if filters.limit == n %} selected{% endif %}>{{ n }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="muted">Страница</label>
|
||||
<input type="text" name="page" value="{{ filters.page }}" style="width:90px;">
|
||||
</div>
|
||||
<div style="display:flex; align-items:flex-end;">
|
||||
<button class="btn btn-primary" type="submit">Применить</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="pagination">
|
||||
{% with q=filters.q role=filters.role active=filters.active domain=filters.domain sort=filters.sort limit=filters.limit %}
|
||||
{% if page.has_prev %}
|
||||
<a href="?q={{ q }}&role={{ role }}&active={{ active }}&domain={{ domain }}&sort={{ sort }}&limit={{ limit }}&page={{ page.page|add:"-1" }}">« Предыдущая</a>
|
||||
{% else %}
|
||||
<span class="disabled">« Предыдущая</span>
|
||||
{% endif %}
|
||||
<span>Стр. {{ page.page }}</span>
|
||||
{% if page.has_next %}
|
||||
<a href="?q={{ q }}&role={{ role }}&active={{ active }}&domain={{ domain }}&sort={{ sort }}&limit={{ limit }}&page={{ page.page|add:"1" }}">Следующая »</a>
|
||||
{% else %}
|
||||
<span class="disabled">Следующая »</span>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
{% if error %}
|
||||
<div class="messages"><li class="error">{{ error }}</li></div>
|
||||
{% endif %}
|
||||
|
||||
<div class="list">
|
||||
{% for p in profiles %}
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="grow">
|
||||
<div style="font-weight:700; font-size:16px;">{{ p.name }}</div>
|
||||
|
||||
</div>
|
||||
<div>
|
||||
<form method="post" action="{% url 'like_profile' p.id %}">
|
||||
{% csrf_token %}
|
||||
{% include "ui/components/like_button.html" with profile_id=p.id liked=p.liked %}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="meta">
|
||||
<div><span class="pill">{{ p.verified|yesno:"ACTIVE,INACTIVE" }}</span></div>
|
||||
<div class="pill">{{ p.role|default:"USER" }}</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row" style="margin-top:10px;">
|
||||
<div class="grow"></div>
|
||||
<a class="btn" href="{% url 'profile_detail' p.id %}">Открыть</a>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="card">
|
||||
<div class="muted">Ничего не найдено. Попробуйте изменить фильтры.</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="pagination">
|
||||
{% with q=filters.q role=filters.role active=filters.active domain=filters.domain sort=filters.sort limit=filters.limit %}
|
||||
{% if page.has_prev %}
|
||||
<a href="?q={{ q }}&role={{ role }}&active={{ active }}&domain={{ domain }}&sort={{ sort }}&limit={{ limit }}&page={{ page.page|add:"-1" }}">« Предыдущая</a>
|
||||
{% else %}
|
||||
<span class="disabled">« Предыдущая</span>
|
||||
{% endif %}
|
||||
<span>Стр. {{ page.page }}</span>
|
||||
{% if page.has_next %}
|
||||
<a href="?q={{ q }}&role={{ role }}&active={{ active }}&domain={{ domain }}&sort={{ sort }}&limit={{ limit }}&page={{ page.page|add:"1" }}">Следующая »</a>
|
||||
{% else %}
|
||||
<span class="disabled">Следующая »</span>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
24
templates/ui/register.html
Normal file
24
templates/ui/register.html
Normal file
@@ -0,0 +1,24 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Регистрация — MatchAgency{% endblock %}
|
||||
{% block content %}
|
||||
<div class="max-w-md mx-auto rounded-xl bg-white/80 backdrop-blur border shadow p-6">
|
||||
<h1 class="text-xl font-semibold mb-4">Регистрация</h1>
|
||||
<form action="" method="post" class="space-y-3">
|
||||
{% csrf_token %}
|
||||
<div>
|
||||
<label class="block text-sm mb-1">Email</label>
|
||||
<input type="email" name="email" required class="w-full rounded-md border px-3 py-2"/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm mb-1">Пароль</label>
|
||||
<input type="password" name="password" required class="w-full rounded-md border px-3 py-2"/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm mb-1">Полное имя (необязательно)</label>
|
||||
<input type="text" name="full_name" class="w-full rounded-md border px-3 py-2"/>
|
||||
</div>
|
||||
<button class="w-full rounded-md bg-indigo-600 text-white px-4 py-2 hover:bg-indigo-700">Зарегистрироваться</button>
|
||||
</form>
|
||||
<p class="mt-3 text-sm">Уже есть аккаунт? <a class="text-indigo-700 hover:underline" href="{% url 'login' %}">Войти</a></p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user