Compare commits

...

26 Commits
first ... main

Author SHA1 Message Date
f4bef815be Merge pull request 'Поменял папку' (#10) from fix_test into main
Some checks failed
Auto-update README / update-readme (push) Has been cancelled
Reviewed-on: #10
2026-04-24 22:33:23 +03:00
0134941e0f Merge branch 'main' into fix_test 2026-04-24 22:33:18 +03:00
NikDizell
ed584ac3de Merge branch 'fix_test' of https://git.nikdizell.ru/SNA_Tecnology/Site into fix_test 2026-04-24 22:32:22 +03:00
NikDizell
f710301c00 Поменял папку 2026-04-24 22:31:57 +03:00
NikDizell
4b671736ce Поменял папку 2026-04-24 22:30:39 +03:00
c9aac145cf Merge pull request 'Фикс' (#9) from fix_test into main
Reviewed-on: #9
2026-04-24 22:28:13 +03:00
NikDizell
412dfbffac Фикс 2026-04-24 22:27:43 +03:00
6e0e367569 Merge pull request 'Добавил авто описание' (#8) from fix_test into main
Reviewed-on: #8
2026-04-24 22:22:53 +03:00
NikDizell
dd9be8feb5 Добавил авто описание 2026-04-24 22:22:23 +03:00
9982c682c3 Merge pull request 'Фикс' (#7) from fix_test into main
Reviewed-on: #7
2026-04-24 15:26:00 +03:00
NikDizell
f72eb0246a Фикс 2026-04-24 15:25:28 +03:00
4ac7705f71 Merge pull request 'Поменял титл' (#6) from fix_test into main
Reviewed-on: #6
2026-04-24 15:11:24 +03:00
NikDizell
59c09011ea Поменял титл 2026-04-24 15:10:38 +03:00
NikDizell
70077ed279 Merge branch 'fix_test' 2026-04-24 14:57:29 +03:00
NikDizell
9eed6b578b Собрал коллекции 2026-04-24 14:55:23 +03:00
NikDizell
506ec6e2f9 Добавил реквизиты организации и полностью переработал концепцию сайта, сделал его на компанию, а не на частного 2026-04-24 14:54:31 +03:00
NikDizell
53d97f674e Собрал коллекции 2026-04-09 19:16:01 +03:00
NikDizell
d974bbd740 Merge branch 'fix_test' 2026-04-09 19:10:16 +03:00
NikDizell
b9a6e2a95b Исправил импорт 2026-04-09 19:09:19 +03:00
8462c66f72 Merge pull request 'Делаем слаг обязательным' (#5) from fix_test into main
Reviewed-on: #5
2026-04-09 19:01:42 +03:00
NikDizell
dffb41780a Делаем слаг обязательным 2026-04-09 19:00:37 +03:00
dd99fdffe8 Merge pull request 'fix_test' (#4) from fix_test into main
Reviewed-on: #4
2026-04-09 18:47:18 +03:00
NikDizell
223fea5369 Добавил слаг и другиек мелкие изменения 2026-04-09 18:44:02 +03:00
NikDizell
df0e27696c Переделал отображение статей решений и другие мелкие правки 2026-04-09 16:48:37 +03:00
NikDizell
cf0d48c800 Переделал админку 2026-04-09 14:58:18 +03:00
NikDizell
443d547f9d Добавил капчу на форму регистрации 2026-04-08 00:02:08 +03:00
25 changed files with 1695 additions and 264 deletions

View File

@ -0,0 +1,29 @@
name: Auto-update README
on:
push:
branches: [main]
jobs:
update-readme:
runs-on: ubuntu-latest
steps:
- name: Клонировать репозиторий
uses: actions/checkout@v4
with:
token: ${{ secrets.ACTIONS_TOKEN }}
- name: Запустить Python-скрипт
run: python3 scripts/update_readme.py
- name: Закоммитить изменения, если есть
run: |
git config user.name "README Bot"
git config user.email "bot@example.com"
if git diff --quiet README.md; then
echo "Изменений нет"
else
git add README.md
git commit -m "Автообновление README [skip ci]"
git push
fi

View File

@ -15,3 +15,7 @@ pip install -r requirements.txt
python manage.py migrate
python manage.py collectstatic
python manage.py createsuperuser
## Структура проекта (автообновление)
<!-- BEGIN_STRUCTURE -->
<!-- END_STRUCTURE -->

View File

@ -60,15 +60,12 @@
{% endif %}
</div>
<!-- Скрипт для модального окна -->
<script src="{% static 'programmer/js/recall.js' %}"></script>
<!-- Модальное окно для увеличения изображений -->
<div id="imageModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3 id="modalTitle">Просмотр изображения</h3>
<button class="modal-close" onclick="closeModal()">&times;</button>
<button class="modal-close" onclick="ImageModal.close()">&times;</button>
</div>
<div class="modal-body">
<img class="modal-image" id="modalImage" alt="">
@ -76,6 +73,9 @@
</div>
</div>
<!-- Скрипт для модального окна -->
<script src="{% static 'programmer/js/recall.js' %}"></script>
{% endblock %}
{% block extra_js %}
@ -90,7 +90,7 @@
img.style.cursor = 'pointer';
img.addEventListener('click', function() {
// Вызываем глобальную функцию openModal из recall.js
openModal(this.src, this.alt || 'Изображение из статьи');
ImageModal.open(this.src, this.alt || 'Изображение из статьи');
});
});
});

View File

@ -3,7 +3,8 @@ from django.utils import timezone
from django.utils.html import format_html
from django.urls import path
from django.shortcuts import render
from django.contrib import messages
from django.db import models
from django_ckeditor_5.widgets import CKEditor5Widget
from .models import Home, Solution, Competence, Recall, CallbackRequest, PageView
@ -11,38 +12,134 @@ from .models import Home, Solution, Competence, Recall, CallbackRequest, PageVie
# ===== BASE ADMIN CLASSES =====
class BaseContentAdmin(admin.ModelAdmin):
"""Общий базовый класс для контентных моделей."""
"""
Базовый класс для контентных моделей.
CKEditor, actions публикации, date_hierarchy.
"""
list_display_links = ('id', 'title')
list_editable = ('is_published',)
list_filter = ('time_create', 'is_published')
search_fields = ('title',)
date_hierarchy = 'time_create'
actions = ['make_published', 'make_unpublished']
readonly_fields = ('time_create', 'time_update')
formfield_overrides = {
models.TextField: {'widget': CKEditor5Widget(config_name='default')},
}
def make_published(self, request, queryset):
updated = queryset.update(is_published=True)
self.message_user(request, f'{updated} объектов опубликовано.')
make_published.short_description = 'Опубликовать выбранные'
def make_unpublished(self, request, queryset):
updated = queryset.update(is_published=False)
self.message_user(request, f'{updated} объектов снято с публикации.')
make_unpublished.short_description = 'Снять с публикации'
# ===== MODEL ADMINS =====
@admin.register(Home)
class HomeAdmin(BaseContentAdmin):
# Поля: title, content, home_image, time_create, time_update, is_published
list_display = ('id', 'title', 'time_create', 'is_published')
search_fields = ('title', 'content')
fieldsets = (
(None, {
'fields': ('title', 'is_published'),
}),
('Содержание', {
'fields': ('content', 'home_image'),
'description': 'Основной текст главной страницы.',
}),
('Служебное', {
'fields': ('time_create', 'time_update'),
'classes': ('collapse',),
}),
)
@admin.register(Solution)
class SolutionAdmin(BaseContentAdmin):
# Поля: title, description, implementation, closing, time_create, time_update, is_published
list_display = ('id', 'title', 'time_create', 'is_published')
list_display_links = ('id', 'title')
search_fields = ('title', 'description', 'implementation')
prepopulated_fields = {'slug': ('title',)}
fieldsets = (
(None, {
'fields': ('title', 'slug', 'is_published'),
}),
('Содержание', {
'fields': ('description', 'implementation', 'closing'),
'description': 'Описание, реализация и заключение по проекту.',
}),
('Служебное', {
'fields': ('time_create', 'time_update'),
'classes': ('collapse',),
}),
)
@admin.register(Competence)
class CompetenceAdmin(BaseContentAdmin):
# Поля: title, content, photo, time_create, time_update, is_published
list_display = ('id', 'title', 'time_create', 'is_published')
search_fields = ('title', 'content')
fieldsets = (
(None, {
'fields': ('title', 'is_published'),
}),
('Содержание', {
'fields': ('content', 'photo'),
'description': 'Описание компетенции и фото.',
}),
('Служебное', {
'fields': ('time_create', 'time_update'),
'classes': ('collapse',),
}),
)
@admin.register(Recall)
class RecallAdmin(BaseContentAdmin):
list_display = ('id', 'title', 'time_create', 'scan', 'is_published')
# Поля: title, content, scan, time_create, time_update, is_published
list_display = ('id', 'title', 'time_create', 'scan_preview', 'is_published')
search_fields = ('title', 'content')
fieldsets = (
(None, {
'fields': ('title', 'is_published'),
}),
('Содержание', {
'fields': ('content', 'scan', 'scan_preview'),
'description': 'Текст отзыва и скан документа.',
}),
('Служебное', {
'fields': ('time_create', 'time_update'),
'classes': ('collapse',),
}),
)
readonly_fields = ('time_create', 'time_update', 'scan_preview')
def scan_preview(self, obj):
if obj.scan:
return format_html(
'<img src="{}" style="max-height:200px; max-width:400px; '
'border-radius:4px; border:1px solid #ddd;" />',
obj.scan.url
)
return ''
scan_preview.short_description = 'Превью скана'
# ===== CALLBACK =====
@admin.register(CallbackRequest)
class CallbackAdmin(admin.ModelAdmin):
@ -54,16 +151,12 @@ class CallbackAdmin(admin.ModelAdmin):
readonly_fields = ('time_create',)
actions = ['mark_as_read', 'mark_as_unread', 'mark_as_processed', 'resend_notification']
# ── badge helper ──────────────────────────────────────────────────────────
def new_badge(self, obj):
if not obj.is_read:
return format_html('<span style="color:red;font-weight:bold;">🆕 НОВАЯ</span>')
return ''
new_badge.short_description = 'Статус'
# ── bulk actions ──────────────────────────────────────────────────────────
def mark_as_read(self, request, queryset):
updated = queryset.update(is_read=True)
self.message_user(request, f'{updated} заявок отмечены как прочитанные')
@ -85,8 +178,6 @@ class CallbackAdmin(admin.ModelAdmin):
self.message_user(request, f'Уведомления отправлены для {count} заявок')
resend_notification.short_description = 'Переотправить email уведомления'
# ── custom URL + view for stats ───────────────────────────────────────────
def get_urls(self):
urls = super().get_urls()
custom_urls = [
@ -117,11 +208,8 @@ class CallbackAdmin(admin.ModelAdmin):
}
return render(request, 'admin/callback_stats.html', context)
# ── FIX: removed get_queryset message_user spam ───────────────────────────
# Previously, a warning banner was shown on EVERY request to the changelist,
# including background requests. Removed entirely — the new_badge column
# and base_site.html header notification already surface unread counts.
# ===== PAGE VIEWS =====
@admin.register(PageView)
class PageViewAdmin(admin.ModelAdmin):
@ -134,7 +222,6 @@ class PageViewAdmin(admin.ModelAdmin):
def get_queryset(self, request):
return super().get_queryset(request).order_by('-timestamp')
# PageViews should never be created or edited manually
def has_add_permission(self, request):
return False

View File

@ -78,6 +78,7 @@ class ProfileForm(forms.ModelForm):
class RegistrationForm(UserCreationForm):
captcha = TurnstileField(label='', theme='light', size='normal')
# Поля пользователя
first_name = forms.CharField(
max_length=30,

View File

@ -24,24 +24,42 @@ class MenuContextMixin(ContextMixin):
# Основное меню (всегда)
main_menu = [
{'title': "Главная", 'url_name': 'home'},
{'title': "Проекты", 'url_name': 'solution'},
# {'title': "Главная", 'url_name': 'home'},
{
'title': "Разработки",
'url_name': None,
'children': [
{'title': "Кейсы", 'url_name': 'solution'},
{'title': "Статьи", 'url_name': 'blog'},
]
},
{'title': "Отзывы", 'url_name': 'recall'},
{'title': "Обо мне", 'url_name': 'about'},
{'title': "О нас", 'url_name': 'about'},
]
# Пользовательское меню (зависит от статуса)
if self.request.user.is_authenticated:
user_menu = [
{
'title': "Профиль",
'url_name': None,
'children': [
{'title': "Профиль", 'url_name': 'profile'},
{'title': "Выйти", 'url_name': 'logout'}, # будет обработан как форма
{'title': "Выйти", 'url_name': 'logout'},
]
}
]
else:
user_menu = [
{
'title': "Войти",
'url_name': None,
'children': [
{'title': "Войти", 'url_name': 'login'},
{'title': "Регистрация", 'url_name': 'register'},
]
}
]
context['main_menu'] = main_menu
context['user_menu'] = user_menu

View File

@ -1,3 +1,5 @@
import logging
from django.contrib.auth.models import User
from django.db import models
from django.urls import reverse
@ -6,12 +8,15 @@ from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from django.core.cache import cache
from .utils.email_notifications import send_callback_notification
from django.utils.text import slugify
logger = logging.getLogger(__name__)
class Recall(models.Model):
title = models.CharField(max_length=255, verbose_name='Организация')
content = models.TextField(blank=True, verbose_name='Отзыв')
scan = models.ImageField(upload_to="scan/%Y/%m/%d/", verbose_name='Фото')
scan = models.ImageField(upload_to="scan/%Y/%m/%d/", blank=True, null=True, verbose_name='Фото')
time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')
time_update = models.DateTimeField(auto_now=True, verbose_name='Дата изменения')
is_published = models.BooleanField(default=True, verbose_name='Опубликован')
@ -40,7 +45,7 @@ class Recall(models.Model):
class Competence(models.Model):
title = models.CharField(max_length=255, verbose_name='Программист')
content = models.TextField(blank=True, verbose_name='Компетенция')
photo = models.ImageField(upload_to="photos/%Y/%m/%d/", verbose_name='Фото')
photo = models.ImageField(upload_to="photos/%Y/%m/%d/", blank=True, null=True, verbose_name='Фото')
time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')
time_update = models.DateTimeField(auto_now=True, verbose_name='Дата изменения')
is_published = models.BooleanField(default=True, verbose_name='Опубликован')
@ -65,12 +70,23 @@ class Solution(models.Model):
time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')
time_update = models.DateTimeField(auto_now=True, verbose_name='Дата изменения')
is_published = models.BooleanField(default=True, verbose_name='Опубликован')
slug = models.SlugField(
max_length=255,
unique=True,
db_index=True,
verbose_name='URL-идентификатор',
blank=True,
)
def __str__(self):
return self.title
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.title)
super().save(*args, **kwargs)
def get_absolute_url(self):
return reverse('solution_detail', kwargs={'pk': self.pk})
return reverse('solution_detail', kwargs={'slug': self.slug})
class Meta:
verbose_name = 'Проекты'
@ -78,18 +94,17 @@ class Solution(models.Model):
ordering = ['time_create', 'title']
def get_seo_title(self):
"""Генерирует SEO-заголовок для проекта"""
return f"Проект: {self.title} | Автоматизация 1С"
def get_seo_description(self):
"""Генерирует SEO-описание для проекта"""
if self.description:
clean_desc = self.description[:160].replace('\n', ' ').strip()
return f"Проект автоматизации: {self.title}. {clean_desc}..."
# Используем closing если есть, иначе description
source = self.closing or self.description
if source:
clean = source[:160].replace('\n', ' ').strip()
return f"Проект автоматизации: {self.title}. {clean}..."
return f"Реализация проекта {self.title} - программист 1С Николай Сердюк"
def get_meta_keywords(self):
"""Автоматические ключевые слова для проекта"""
base_keywords = ["проект 1С", "автоматизация 1С", "внедрение 1С"]
title_words = self.title.lower().split()
return base_keywords + title_words
@ -98,7 +113,7 @@ class Solution(models.Model):
class Home(models.Model):
title = models.CharField(max_length=255, verbose_name='Наименование')
content = models.TextField(blank=True, verbose_name='Статья')
home_image = models.ImageField(upload_to="home_image/%Y/%m/%d/", verbose_name='Фото')
home_image = models.ImageField(upload_to="home_image/%Y/%m/%d/", blank=True, null=True, verbose_name='Фото')
time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')
time_update = models.DateTimeField(auto_now=True, verbose_name='Дата изменения')
is_published = models.BooleanField(default=True, verbose_name='Опубликован')
@ -118,8 +133,8 @@ class Home(models.Model):
class CallbackRequest(models.Model):
name = models.CharField(max_length=100, verbose_name='Имя')
phone = models.CharField(max_length=20, verbose_name='Телефон')
email = models.EmailField(blank=True, null=True, verbose_name='Электронная почта') # Сделать необязательным
question = models.TextField(blank=True, verbose_name='Ваш вопрос') # Сделать необязательным
email = models.EmailField(blank=True, null=True, verbose_name='Электронная почта')
question = models.TextField(blank=True, verbose_name='Ваш вопрос')
time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')
is_processed = models.BooleanField(default=False, verbose_name='Обработано')
is_read = models.BooleanField(default=False, verbose_name='Прочитано')
@ -134,16 +149,18 @@ class CallbackRequest(models.Model):
ordering = ['-time_create']
# Сигнал для отправки уведомления при создании заявки
@receiver(post_save, sender=CallbackRequest)
def send_callback_email_notification(sender, instance, created, **kwargs):
if created and not instance.notification_sent:
# Отправляем email уведомление
try:
success = send_callback_notification(instance)
if success:
instance.notification_sent = True
# Сохраняем без повторного вызова сигнала
sender.objects.filter(pk=instance.pk).update(notification_sent=True)
except Exception:
# Логируем ошибку, но не прерываем сохранение объекта
logger.exception(
'Не удалось отправить email-уведомление для заявки pk=%s', instance.pk
)
class PageView(models.Model):
@ -190,7 +207,6 @@ class Profile(models.Model):
@receiver(post_save, sender=User)
def create_or_save_user_profile(sender, instance, created, **kwargs):
# Получаем или создаём профиль, затем сохраняем
if created:
Profile.objects.get_or_create(user=instance)
@ -200,5 +216,4 @@ def create_or_save_user_profile(sender, instance, created, **kwargs):
@receiver([post_save, post_delete], sender=Competence)
@receiver([post_save, post_delete], sender=Recall)
def clear_sitemap_cache(sender, **kwargs):
"""Очищаем кэш sitemap при изменении контента"""
cache.delete('sitemap_cache')

View File

@ -1,10 +1,11 @@
import logging
from typing import Optional
from django.http import HttpRequest
from .models import PageView, Visitor
from django.utils import timezone
from typing import Type
from django.db.models import Model, QuerySet
logger = logging.getLogger(__name__)
def get_client_ip(request: HttpRequest) -> str:
@ -22,13 +23,11 @@ def should_track_request(request: HttpRequest) -> bool:
client_ip = get_client_ip(request)
path = request.path
# Игнорируемые пути
ignored_paths = [
'/static/', '/admin/', '/index.php', '/status.php',
'/cron', '/remote.php', '/ocs', '/apps/', '/custom_apps/',
]
# Игнорируемые IP (Docker сети)
docker_ips = [
'192.168.64.1', '192.168.65.1',
'172.17.0.1', '172.18.0.1', '172.19.0.1',
@ -48,7 +47,6 @@ def track_page_view(request: HttpRequest) -> None:
return
try:
# Сохраняем просмотр страницы
PageView.objects.create(
url=request.path,
ip_address=get_client_ip(request),
@ -56,13 +54,12 @@ def track_page_view(request: HttpRequest) -> None:
referer=request.META.get('HTTP_REFERER', '')[:500],
)
# Обновляем статистику посетителя
ip = get_client_ip(request)
visitor, created = Visitor.objects.get_or_create(
ip_address=ip,
defaults={
'first_visit': timezone.now(),
'last_visit': timezone.now()
'last_visit': timezone.now(),
}
)
@ -71,26 +68,14 @@ def track_page_view(request: HttpRequest) -> None:
visitor.visit_count += 1
visitor.save()
except Exception as e:
# В продакшене лучше использовать логирование
print(f"Error tracking page view: {e}")
except Exception:
logger.exception('Ошибка при трекинге просмотра страницы: %s', request.path)
def get_published_queryset(model_class, order_by: str = '-time_create'):
"""
Возвращает QuerySet опубликованных записей для модели.
Args:
model_class: Класс модели Django
order_by: Поле для сортировки
Returns:
QuerySet или пустой список, если поле is_published отсутствует
"""
if hasattr(model_class, 'is_published'):
return model_class.objects.filter(is_published=True).order_by(order_by)
return model_class.objects.none()

View File

@ -0,0 +1,194 @@
/* ===== PROJECTS GRID ===== */
.projects-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
/* ===== PROJECT CARD ===== */
.project-card {
background: var(--bg-card);
border-radius: var(--radius-lg);
border: 1px solid var(--border-light);
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
transition: transform 0.25s ease, box-shadow 0.25s ease, border-color 0.25s ease;
}
.project-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
border-color: var(--primary-light);
}
/* Оранжевая полоска сверху — единственный декоративный слой */
.project-card__accent {
height: 3px;
background: var(--gradient-primary);
flex-shrink: 0;
}
.project-card__body {
padding: 1.5rem 1.5rem 1rem;
flex: 1;
}
.project-card__title {
font-size: 1.125rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 0.75rem;
line-height: 1.4;
}
.project-card__desc {
font-size: 0.95rem;
color: var(--text-secondary);
line-height: 1.65;
margin: 0;
}
.project-card__footer {
padding: 1rem 1.5rem 1.25rem;
border-top: 1px solid var(--border-light);
display: flex;
align-items: center;
}
.project-card__link {
display: inline-flex;
align-items: center;
gap: 0.4rem;
color: var(--primary);
font-weight: 600;
font-size: 0.9rem;
text-decoration: none;
transition: gap 0.2s ease, color 0.2s ease;
}
.project-card__link:hover {
color: var(--primary-dark);
gap: 0.65rem;
}
/* ===== MOBILE ===== */
@media (max-width: 768px) {
.projects-grid {
grid-template-columns: 1fr;
gap: 1rem;
}
/* На мобиле убираем hover-подъём — он не работает на touch */
.project-card:hover {
transform: none;
box-shadow: none;
}
/* Вместо этого — активное состояние при нажатии */
.project-card:active {
border-color: var(--primary);
}
.project-card__body {
padding: 1.25rem 1.25rem 0.75rem;
}
.project-card__footer {
padding: 0.75rem 1.25rem 1rem;
}
.project-card__title {
font-size: 1rem;
}
}
@media (max-width: 480px) {
.projects-grid {
gap: 0.75rem;
}
}
/* ===== СТИЛИ ДЛЯ СТРАНИЦЫ ДЕТАЛЬНОГО РЕШЕНИЯ ===== */
.solution-section {
margin-bottom: 2.5rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid var(--border-light);
}
.solution-section:last-of-type {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.section-title {
font-size: 1.75rem;
font-weight: 700;
margin-bottom: 1.5rem;
color: var(--primary);
display: flex;
align-items: center;
gap: 0.5rem;
border-left: 5px solid var(--primary);
padding-left: 1.25rem;
}
.section-content {
font-size: 1.05rem;
line-height: 1.7;
color: var(--text-secondary);
}
/* Вложенные заголовки */
.section-content h2,
.section-content h3 {
margin-top: 1.5em;
margin-bottom: 0.75em;
color: var(--text-primary);
font-weight: 600;
}
.section-content h2 {
font-size: 1.4rem;
border-bottom: 1px dashed var(--border-light);
padding-bottom: 0.5rem;
}
.section-content h3 {
font-size: 1.2rem;
}
.section-content ul,
.section-content ol {
margin: 1rem 0 1rem 1.5rem;
}
.section-content li {
margin-bottom: 0.5rem;
}
/* Адаптация для мобильных */
@media (max-width: 768px) {
.solution-section {
margin-bottom: 2rem;
padding-bottom: 1rem;
}
.section-title {
font-size: 1.5rem;
padding-left: 1rem;
border-left-width: 4px;
}
.section-content {
font-size: 1rem;
}
}
@media (max-width: 480px) {
.section-title {
font-size: 1.35rem;
}
}

View File

@ -125,7 +125,7 @@ body {
.nav {
display: flex;
align-items: center;
justify-content: space-between;
justify-content: flex-start;
padding: 1rem 0;
gap: 2rem;
}
@ -160,16 +160,190 @@ body {
.nav-menu {
display: flex;
align-items: center;
list-style: none;
gap: 2.5rem;
margin: 0;
flex-wrap: wrap;
}
/* ===== DROPDOWN MENU (DESKTOP) ===== */
.nav-menu .has-dropdown {
position: relative;
}
/* Родительский пункт (span или ссылка) */
.nav-menu .has-dropdown > a,
.nav-menu .has-dropdown > span {
text-decoration: none;
color: var(--text-secondary);
font-weight: 600;
padding: 0.75rem 0;
position: relative;
font-size: 1rem;
display: inline-block;
cursor: default;
transition: var(--transition);
}
/* Выпадающий список */
.nav-menu .dropdown {
position: absolute;
top: 100%;
left: 0;
min-width: 200px;
background: var(--bg-card);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-xl);
border: 1px solid var(--border-light);
opacity: 0;
visibility: hidden;
transform: translateY(-10px);
transition: opacity 0.2s ease, visibility 0.2s ease, transform 0.2s ease;
list-style: none;
padding: 0.5rem 0;
z-index: 1000;
}
/* Показываем dropdown при наведении на родительский li */
.nav-menu .has-dropdown:hover .dropdown {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
/* Элементы dropdown */
.nav-menu .dropdown li {
margin: 0;
}
.nav-menu .dropdown a {
display: block;
padding: 0.6rem 1.2rem;
color: var(--text-primary);
text-decoration: none;
transition: var(--transition);
font-weight: 500;
white-space: nowrap;
}
.nav-menu .dropdown a:hover {
background: var(--bg-secondary);
color: var(--primary);
padding-left: 1.5rem;
}
/* Небольшой треугольник (опционально) */
.nav-menu .has-dropdown > span::after,
.nav-menu .has-dropdown > a::after {
/* content: '▼'; */
font-size: 0.75em;
margin-left: 6px;
opacity: 0.7;
vertical-align: middle;
transition: transform 0.2s ease;
}
.nav-menu .has-dropdown:hover > span::after,
.nav-menu .has-dropdown:hover > a::after {
transform: rotate(180deg);
}
/* ===== USER MENU DROPDOWN (DESKTOP) ===== */
.user-menu {
display: flex;
align-items: center;
gap: 0.5rem;
}
.user-menu-item {
position: relative;
}
.user-menu-parent {
cursor: default;
display: inline-block;
padding: 0.5rem 1rem; /* соответствие .nav-link */
}
.user-menu .has-dropdown > a,
.user-menu .has-dropdown > span {
cursor: default;
}
.user-menu .dropdown {
position: absolute;
top: 100%;
right: 0; /* выравнивание по правому краю */
min-width: 180px;
background: var(--bg-card);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-xl);
border: 1px solid var(--border-light);
opacity: 0;
visibility: hidden;
transform: translateY(-10px);
transition: opacity 0.2s ease, visibility 0.2s ease, transform 0.2s ease;
list-style: none;
padding: 0.5rem 0;
z-index: 1001;
}
.user-menu .has-dropdown:hover .dropdown {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.user-menu .dropdown li {
margin: 0;
}
.user-menu .dropdown-link,
.user-menu .dropdown-link-btn {
display: block;
width: 100%;
padding: 0.6rem 1.2rem;
color: var(--text-primary);
text-decoration: none;
transition: var(--transition);
font-weight: 500;
white-space: nowrap;
background: none;
border: none;
text-align: left;
font: inherit;
cursor: pointer;
}
.user-menu .dropdown-link:hover,
.user-menu .dropdown-link-btn:hover {
background: var(--bg-secondary);
color: var(--primary);
padding-left: 1.5rem;
}
/* Стрелка для родительского пункта (опционально) */
.user-menu .has-dropdown > span::after,
.user-menu .has-dropdown > a::after {
/* content: '▼'; */
font-size: 0.75em;
margin-left: 6px;
opacity: 0.7;
vertical-align: middle;
transition: transform 0.2s ease;
}
.user-menu .has-dropdown:hover > span::after,
.user-menu .has-dropdown:hover > a::after {
transform: rotate(180deg);
}
.nav-actions {
display: flex;
align-items: center;
gap: 1rem;
margin-left: auto;
}
.nav-link {
@ -1346,6 +1520,53 @@ body {
margin-bottom: 0.5rem;
}
/* ===== MOBILE SUBMENU ===== */
.mobile-nav-item.has-submenu {
position: relative;
}
/* Родительский пункт в мобильном меню (может быть span или ссылка) */
.mobile-nav-parent {
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
}
/* Стрелочка для родительского пункта */
.mobile-nav-parent::after {
content: '▼';
font-size: 0.8rem;
opacity: 0.7;
transition: transform 0.2s ease;
}
.has-submenu.submenu-open .mobile-nav-parent::after {
transform: rotate(180deg);
}
/* Вложенный список */
.mobile-submenu {
list-style: none;
margin-left: 1rem;
padding-left: 0.5rem;
border-left: 2px solid var(--primary);
display: none;
}
.has-submenu.submenu-open .mobile-submenu {
display: block;
}
.mobile-submenu .mobile-nav-item {
margin-bottom: 0;
}
.mobile-submenu .mobile-nav-link {
padding: 0.75rem 1rem;
font-size: 0.95rem;
}
.mobile-nav-link {
display: block;
padding: 1rem;
@ -1753,3 +1974,38 @@ body {
animation: pulse 1.5s infinite;
will-change: transform;
}
/* Стили для <details> / <summary> */
details {
margin: 1rem 0;
}
summary {
display: inline-block;
padding: 0.5rem 1rem;
background: var(--bg-primary);
border: 1px solid var(--border-light);
border-radius: var(--radius-md);
transition: var(--transition);
user-select: none;
}
summary:hover {
background: var(--primary);
color: white;
border-color: var(--primary);
}
details[open] summary {
background: var(--primary);
color: white;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
details[open] {
border: 1px solid var(--border-light);
border-radius: var(--radius-md);
padding: 1rem;
background: var(--bg-primary);
}

View File

@ -49,9 +49,14 @@ document.addEventListener('DOMContentLoaded', function() {
}
// Закрытие меню при клике на ссылку
const mobileNavLinks = document.querySelectorAll('.mobile-nav-link');
mobileNavLinks.forEach(link => {
link.addEventListener('click', closeMobileMenu);
document.querySelectorAll('.mobile-nav-item.has-submenu > .mobile-nav-link, .mobile-nav-item.has-submenu > .mobile-nav-parent').forEach(item => {
item.addEventListener('click', function(e) {
if (window.innerWidth <= 768) { // или ваш брейкпоинт
e.preventDefault();
const parent = this.closest('.has-submenu');
parent.classList.toggle('submenu-open');
}
});
});
// Синхронизация переключателей темы

View File

@ -4,88 +4,133 @@
{% block content %}
<div class="page-header">
<h1 class="page-title">{{title}}</h1>
<p class="page-subtitle">Профессиональный программист 1С с более чем 10-летним опытом</p>
<h1 class="page-title">{{ title }}</h1>
<p class="page-subtitle">Команда экспертов по автоматизации бизнеса на платформе 1С</p>
</div>
<div class="content-card">
<div class="about-header">
<h2>Николай Сердюк</h2>
<p class="subtitle">Разработчик 1С</p>
<!-- <div class="about-header">
<h2>С.Н.А. Технологии</h2>
<p class="subtitle">Профессиональная разработка и сопровождение 1С</p>
</div> -->
<div class="about-section">
<h3>🚀 О компании</h3>
<p class="card-subtitle">
Мы помогаем компаниям расти за счёт прозрачной и эффективной автоматизации
на платформе 1С. Более 10 лет опыта, десятки успешных проектов и
собственная методология внедрения.
</p>
<p>
В основе нашей работы — глубокое понимание бизнес-процессов заказчика,
техническая экспертиза и ответственность за результат. Каждый проект
ведёт выделенная команда под личным управлением ведущего архитектора.
</p>
</div>
<div class="about-section">
<h3>🚀 Опыт работы</h3>
<p class="card-subtitle">Более 10 лет успешной работы в разработке и сопровождении систем на платформе 1С</p>
<div class="experience-item mt-3">
<h4>Основные направления:</h4>
<h3>👥 Наша команда</h3>
<div class="skills-grid">
<div class="skill-category">
<h4>💻 Разработка</h4>
<ul>
<li>Разработка и доработка конфигураций 1С</li>
<li>Создание внешних обработок и отчетов</li>
<li>Кастомизация под бизнес-процессы</li>
<h4>Николай Сердюк</h4>
<p style="margin-bottom: 0.5rem;">Руководитель проектов, ведущий архитектор 1С</p>
<ul style="margin-top: 0;">
<li>10+ лет в разработке 1С</li>
<li>Архитектура сложных интеграций</li>
<li>Управление командой и контроль качества</li>
</ul>
</div>
<div class="skill-category">
<h4>🔗 Интеграция</h4>
<h4>Привлекаемые эксперты</h4>
<ul>
<li>Интеграция 1С с веб-сервисами</li>
<li>Связь с сайтами и мобильными приложениями</li>
<li>API и веб-сервисы</li>
<li>Аналитики бизнес-процессов</li>
<li>Специалисты по интеграции с внешними системами</li>
<!-- <li>Инженеры технической поддержки 24/7</li> -->
<!-- <li>Консультанты по бухгалтерскому и налоговому учёту</li> -->
</ul>
</div>
</div>
<p class="mt-2">
Такой подход позволяет нам гибко масштабировать ресурсы под задачи любой
сложности, сохраняя при этом персональную ответственность и высокое качество.
</p>
</div>
<div class="about-section">
<h3>⚙️ Ключевые компетенции</h3>
<div class="skills-grid">
<div class="skill-category">
<h4>💻 Разработка 1С</h4>
<ul>
<li>Доработка типовых и создание уникальных конфигураций</li>
<li>Внешние обработки, отчёты, печатные формы</li>
<li>Адаптация интерфейсов под бизнес-процессы</li>
</ul>
</div>
<div class="skill-category">
<h4>🔗 Интеграции</h4>
<ul>
<li>Связь 1С с сайтами, маркетплейсами, CRM</li>
<li>Обмен данными через API, веб-сервисы, HTTP-сервисы</li>
<li>Интеграция с банками, платёжными системами, ЕГАИС</li>
</ul>
</div>
<div class="skill-category">
<h4>⚡ Оптимизация</h4>
<ul>
<li>Оптимизация бизнес-процессов</li>
<li>Ускорение работы баз данных</li>
<li>Ускорение работы баз данных и запросов</li>
<li>Автоматизация рутинных операций</li>
<li>Настройка производительности серверов 1С</li>
</ul>
</div>
</div>
</div>
</div>
<div class="about-section">
<h3>🛠 Технологии и навыки</h3>
<h3>🛠 Технологический стек</h3>
<div class="skills-grid">
<div class="skill-category">
<h4>🎯 1С Разработка</h4>
<h4>🎯 Платформа 1С</h4>
<ul>
<li>1С:Предприятие 8.3</li>
<li>Управление торговлей</li>
<li>Бухгалтерия предприятия</li>
<li>Зарплата и управление персоналом</li>
<li>Внешние обработки и отчеты</li>
<li>Управление торговлей (УТ)</li>
<li>Бухгалтерия предприятия (БП)</li>
<li>Зарплата и управление персоналом (ЗУП)</li>
<li>Управление небольшой фирмой (УНФ)</li>
</ul>
</div>
<div class="skill-category">
<h4>🔧 Дополнительные технологии</h4>
<h4>🔧 Смежные технологии</h4>
<ul>
<li>SQL и оптимизация запросов</li>
<li>Веб-сервисы и API</li>
<li>XML, JSON, REST</li>
<li>Системное администрирование</li>
<li>MS SQL, PostgreSQL администрирование и оптимизация</li>
<li>REST API, SOAP, JSON, XML</li>
<li>Git, системы контроля версий</li>
<li>Linux, Windows Server</li>
</ul>
</div>
</div>
</div>
<div class="about-section">
<h3>📈 Проекты и достижения</h3>
<p class="card-subtitle">Успешно реализовал более 50 проектов различной сложности</p>
<h3>📈 Опыт и проекты</h3>
<p class="card-subtitle">Более 50 успешно реализованных проектов для компаний из разных отраслей</p>
<div class="skills-grid mt-3">
<div class="skill-category">
<h4>🏆 Ключевые проекты</h4>
<h4>🏭 Отраслевой опыт</h4>
<ul>
<li>Автоматизация учетных систем для предприятий</li>
<li>Интеграция 1С с сайтами и мобильными приложениями</li>
<li>Разработка кастомизированных отчетов и дашбордов</li>
<li>Оптимизация производительности баз данных</li>
<li>Оптовая и розничная торговля</li>
<li>Производственные предприятия</li>
<li>Логистика и складские комплексы</li>
<li>Сфера услуг</li>
</ul>
</div>
<div class="skill-category">
<h4>📊 Типовые задачи</h4>
<ul>
<li>Автоматизация складского учёта и логистики</li>
<li>Интеграция интернет-магазинов с 1С</li>
<li>Построение управленческой отчётности и дашбордов</li>
<li>Переход с устаревших версий 1С на актуальные</li>
</ul>
</div>
</div>
@ -93,7 +138,6 @@
<div class="about-section">
<h3>📞 Контакты</h3>
<div class="contacts">
<div class="skills-grid">
<div class="skill-category">
<h4>📧 Электронная почта</h4>
@ -105,46 +149,76 @@
</div>
<div class="skill-category">
<h4>💬 Telegram</h4>
<p><strong><a href="https://t.me/odinesina_prog" target="_blank" class="btn btn-primary" style="display: inline-flex; padding: 0.5rem 1rem;">@odinesina_prog</a></strong></p>
</div>
<p>
<a href="https://t.me/odinesina_prog" target="_blank" class="btn btn-primary" style="display: inline-flex; padding: 0.5rem 1rem;">
@odinesina_prog
</a>
</p>
</div>
</div>
<p class="mt-3">
Работаем официально по договору.
<a href="{% url 'requisites' %}" style="color: var(--primary); text-decoration: underline;">
ИП Сердюк Николай Александрович
</a> |
<a href="{% url 'requisites' %}" style="color: var(--primary); text-decoration: underline;">
Реквизиты
</a>
</p>
</div>
<div class="text-center mt-4">
<div class="card-actions">
<a href="{% url 'solution' %}" class="btn btn-primary">📂 Посмотреть мои проекты</a>
<a href="{% url 'solution' %}" class="btn btn-primary">📂 Наши проекты</a>
<a href="{% url 'recall' %}" class="btn btn-secondary">⭐ Отзывы клиентов</a>
</div>
</div>
</div>
{# Обновлённая микроразметка Schema.org для организации #}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Service",
"serviceType": "1С программирование",
"provider": {
"@type": "ProfessionalService",
"name": "С.Н.А. Технологии",
"description": "Профессиональная разработка и сопровождение систем на платформе 1С. Команда экспертов с более чем 10-летним опытом.",
"url": "{{ request.build_absolute_uri }}",
"logo": "{% static 'programmer/images/black_logo.ico' %}",
"telephone": "{{ CONTACT_PHONE }}",
"email": "{{ CONTACT_EMAIL }}",
"address": {
"@type": "PostalAddress",
"addressCountry": "RU"
},
"founder": {
"@type": "Person",
"name": "Николай Сердюк"
"name": "Николай Сердюк",
"jobTitle": "Руководитель проектов"
},
"areaServed": "Россия",
"hasOfferCatalog": {
"@type": "OfferCatalog",
"name": "Услуги программиста 1С",
"name": "Услуги по разработке и сопровождению 1С",
"itemListElement": [
{
"@type": "Offer",
"itemOffered": {
"@type": "Service",
"name": "Разработка конфигураций 1С"
"name": "Разработка и доработка конфигураций 1С"
}
},
{
"@type": "Offer",
"itemOffered": {
"@type": "Service",
"name": "Интеграция 1С с веб-сервисами"
"name": "Интеграция 1С с внешними системами"
}
},
{
"@type": "Offer",
"itemOffered": {
"@type": "Service",
"name": "Техническая поддержка и сопровождение 1С"
}
}
]
@ -155,4 +229,3 @@
<script src="{% static 'programmer/js/floating-button.js' %}"></script>
{% endblock %}

View File

@ -6,13 +6,13 @@
<head>
<title>{{title}}</title>
<!-- Основные мета-теги -->
<meta name="description" content="{% block meta_description %}{{ meta_description|default:'Профессиональный программист 1С с более чем 10-летним опытом. Разработка, интеграция и оптимизация систем 1С.' }}{% endblock %}">
<meta name="description" content="{% block meta_description %}{{ meta_description|default:'Профессиональная разработка и поддержка 1С с более чем 10-летним опытом. Разработка, интеграция и оптимизация систем 1С.' }}{% endblock %}">
<meta name="keywords" content="{% block meta_keywords %}{{ meta_keywords|default:'программист 1С, разработка 1С, интеграция 1С, оптимизация 1С, 1С предприятие' }}{% endblock %}">
<meta name="author" content="Николай Сердюк">
<!-- Open Graph для соцсетей -->
<meta property="og:title" content="{{title}}">
<meta property="og:description" content="{% block og_description %}{{ meta_description|default:'Профессиональный программист 1С с более чем 10-летним опытом' }}{% endblock %}">
<meta property="og:description" content="{% block og_description %}{{ meta_description|default:'Профессиональная разработка и поддержка 1С с более чем 10-летним опытом' }}{% endblock %}">
<meta property="og:type" content="website">
<meta property="og:url" content="{{ request.build_absolute_uri }}">
<!-- <meta property="og:image" content="{% static 'programmer/images/og-image.jpg' %}">-->
@ -21,7 +21,7 @@
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{{title}}">
<meta name="twitter:description" content="{% block twitter_description %}{{ meta_description|default:'Профессиональный программист 1С с более чем 10-летним опытом' }}{% endblock %}">
<meta name="twitter:description" content="{% block twitter_description %}{{ meta_description|default:'Профессиональная разработка и поддержка 1С с более чем 10-летним опытом' }}{% endblock %}">
<!-- <meta name="twitter:image" content="{% static 'programmer/images/og-image.jpg' %}">-->
<!-- Дополнительные SEO-теги -->
@ -90,8 +90,8 @@
"@context": "https://schema.org",
"@type": "Person",
"name": "Николай Сердюк",
"jobTitle": "Программист 1С",
"description": "Профессиональный программист 1С с более чем 10-летним опытом",
"jobTitle": "Компания-разработчик 1С",
"description": "Профессиональная разработка и поддержка 1С с более чем 10-летним опытом",
"url": "https://nikdizell.ru",
"email": "{{ CONTACT_EMAIL }}",
"telephone": "{{ CONTACT_PHONE }}",
@ -122,16 +122,25 @@
<nav class="nav">
<a href="{% url 'home' %}" class="logo">
<img src="{% static 'programmer/images/black_logo.ico' %}" alt="Logo" class="logo-img">
<span class="logo-text">СНА Технологии</span>
<span class="logo-text">С.Н.А. Технологии</span>
</a>
<!-- Десктопное меню -->
<ul class="nav-menu">
{% for item in main_menu %}
<li class="nav-item">
<a href="{% url item.url_name %}" class="nav-link {% if request.resolver_match.url_name == item.url_name %}active{% endif %}">
{{ item.title }}
</a>
<li class="{% if item.children %}has-dropdown{% endif %}">
{% if item.url_name %}
<a href="{% url item.url_name %}" class="nav-link">{{ item.title }}</a>
{% else %}
<span>{{ item.title }}</span>
{% endif %}
{% if item.children %}
<ul class="dropdown">
{% for child in item.children %}
<li><a href="{% url child.url_name %}">{{ child.title }}</a></li>
{% endfor %}
</ul>
{% endif %}
</li>
{% endfor %}
</ul>
@ -157,26 +166,44 @@
<div class="user-menu">
{% for item in user_menu %}
{% if item.url_name == 'logout' %}
<form method="post" action="{% url 'logout' %}" style="display: inline;">
{% csrf_token %}
<button type="submit" class="nav-link btn btn-link" style="border: none; background: none; padding: 0.5rem 1rem; font: inherit; cursor: pointer; color: inherit;">
{{ item.title }}
</button>
</form>
{% elif item.url_name == 'login' %}
<a href="{% url 'login' %}?next={{ request.path|urlencode }}" class="nav-link {% if request.resolver_match.url_name == item.url_name %}active{% endif %}" style="display: inline-block; padding: 0.5rem 1rem;">
{{ item.title }}
</a>
{% elif item.url_name == 'register' %}
<a href="{% url 'register' %}?next={{ request.path|urlencode }}" class="nav-link {% if request.resolver_match.url_name == item.url_name %}active{% endif %}" style="display: inline-block; padding: 0.5rem 1rem;">
<div class="user-menu-item {% if item.children %}has-dropdown{% endif %}">
{% if item.url_name %}
<a href="{% url item.url_name %}" class="nav-link {% if request.resolver_match.url_name == item.url_name %}active{% endif %}">
{{ item.title }}
</a>
{% else %}
<a href="{% url item.url_name %}" class="nav-link {% if request.resolver_match.url_name == item.url_name %}active{% endif %}" style="display: inline-block; padding: 0.5rem 1rem;">
{{ item.title }}
<span class="nav-link user-menu-parent">{{ item.title }}</span>
{% endif %}
{% if item.children %}
<ul class="dropdown user-dropdown">
{% for child in item.children %}
<li>
{% if child.url_name == 'logout' %}
<form method="post" action="{% url 'logout' %}" style="display: block;">
{% csrf_token %}
<button type="submit" class="dropdown-link-btn">
{{ child.title }}
</button>
</form>
{% elif child.url_name == 'login' %}
<a href="{% url 'login' %}?next={{ request.path|urlencode }}" class="dropdown-link">
{{ child.title }}
</a>
{% elif child.url_name == 'register' %}
<a href="{% url 'register' %}?next={{ request.path|urlencode }}" class="dropdown-link">
{{ child.title }}
</a>
{% else %}
<a href="{% url child.url_name %}" class="dropdown-link">
{{ child.title }}
</a>
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endfor %}
</div>
@ -201,35 +228,68 @@
<ul class="mobile-nav-menu">
{% for m in main_menu %}
<li class="mobile-nav-item">
{% if m.url_name == 'logout' %}
<form method="post" action="{% url 'logout' %}" style="display: block;">
{% csrf_token %}
<button type="submit" class="mobile-nav-link btn btn-link" style="border: none; background: none; width: 100%; text-align: left; padding: 1rem; font: inherit; cursor: pointer; color: inherit;">
{{ m.title }}
</button>
</form>
{% else %}
<li class="mobile-nav-item {% if m.children %}has-submenu{% endif %}">
{% if m.url_name %}
<a href="{% url m.url_name %}" class="mobile-nav-link {% if request.resolver_match.url_name == m.url_name %}active{% endif %}">
{{ m.title }}
</a>
{% else %}
<span class="mobile-nav-link mobile-nav-parent">{{ m.title }}</span>
{% endif %}
{% if m.children %}
<ul class="mobile-submenu">
{% for child in m.children %}
<li class="mobile-nav-item">
<a href="{% url child.url_name %}" class="mobile-nav-link {% if request.resolver_match.url_name == child.url_name %}active{% endif %}">
{{ child.title }}
</a>
</li>
{% endfor %}
</ul>
{% endif %}
</li>
{% endfor %}
{% for item in user_menu %}
{% if item.url_name == 'logout' %}
<form method="post" action="{% url 'logout' %}" style="display: inline;">
{% csrf_token %}
<button type="submit" class="nav-link btn btn-link" style="border: none; background: none; padding: 0.5rem 1rem; font: inherit; cursor: pointer; color: inherit;">
{{ item.title }}
</button>
</form>
{% else %}
<a href="{% url item.url_name %}" class="nav-link {% if request.resolver_match.url_name == item.url_name %}active{% endif %}" style="display: inline-block; padding: 0.5rem 1rem;">
<li class="mobile-nav-item {% if item.children %}has-submenu{% endif %}">
{% if item.url_name %}
<a href="{% url item.url_name %}" class="mobile-nav-link {% if request.resolver_match.url_name == item.url_name %}active{% endif %}">
{{ item.title }}
</a>
{% else %}
<span class="mobile-nav-link mobile-nav-parent">{{ item.title }}</span>
{% endif %}
{% if item.children %}
<ul class="mobile-submenu">
{% for child in item.children %}
<li class="mobile-nav-item">
{% if child.url_name == 'logout' %}
<form method="post" action="{% url 'logout' %}" style="display: block;">
{% csrf_token %}
<button type="submit" class="mobile-nav-link" style="border: none; background: none; width: 100%; text-align: left;">
{{ child.title }}
</button>
</form>
{% elif child.url_name == 'login' %}
<a href="{% url 'login' %}?next={{ request.path|urlencode }}" class="mobile-nav-link">
{{ child.title }}
</a>
{% elif child.url_name == 'register' %}
<a href="{% url 'register' %}?next={{ request.path|urlencode }}" class="mobile-nav-link">
{{ child.title }}
</a>
{% else %}
<a href="{% url child.url_name %}" class="mobile-nav-link">
{{ child.title }}
</a>
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
</li>
{% endfor %}
</ul>

View File

@ -1,5 +1,25 @@
{% autoescape off %}
{% for post in posts %}
{% include 'programmer/includes/project_card.html' %}
{% for solution in posts %}
<article class="project-card">
<div class="project-card__accent"></div>
<div class="project-card__body">
<h2 class="card-title">
<a href="{{ solution.get_absolute_url }}">{{ solution.title }}</a>
</h2>
{% if solution.description %}
<p class="project-card__desc">{{ solution.description|striptags|truncatewords:25 }}</p>
{% endif %}
</div>
<div class="project-card__footer">
<a href="{{ solution.get_absolute_url }}" class="project-card__link">
Подробнее
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path d="M3 8h10M9 4l4 4-4 4" stroke="currentColor" stroke-width="1.5"
stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</a>
</div>
</article>
{% endfor %}
{% endautoescape %}

View File

@ -5,7 +5,7 @@
{% block content %}
<div class="hero-section fade-in">
<h1 class="hero-title">🚀 Добро пожаловать!</h1>
<p class="hero-subtitle">Я профессиональный программист 1С с опытом создания эффективных бизнес-решений</p>
<p class="hero-subtitle">Профессиональная разработка на платформе 1С с опытом создания эффективных бизнес-решений</p>
</div>
<div class="grid grid-2">

View File

@ -30,6 +30,8 @@
{% bootstrap_field form.specialization %}
<button type="submit" class="btn btn-primary" style="width: 100%;">Зарегистрироваться</button>
<!-- {{ form.as_p }} -->
{{ form.captcha }}
</form>
<hr>
<p class="text-center">

View File

@ -0,0 +1,50 @@
{% extends 'programmer/base.html' %}
{% load static %}
{% load django_bootstrap5 %}
{% block content %}
<div class="page-header">
<h1 class="page-title">{{ title }}</h1>
<p class="page-subtitle">Официальная информация о компании</p>
</div>
<div class="content-card">
<div class="about-section">
<h3>📄 Реквизиты</h3>
<div style="background: var(--bg-secondary); padding: 2rem; border-radius: var(--radius-lg);">
<p><strong>Индивидуальный предприниматель</strong><br>
Сердюк Николай Александрович</p>
<p><strong>ИНН:</strong> {{ RQ_INN }}</p>
<p><strong>ОГРНИП:</strong> {{ RQ_OGRNIP }}</p>
<p><strong>Юридический адрес:</strong><br>
{{ RQ_ADRES }}</p>
<p><strong>Почтовый адрес:</strong><br>
{{ RQ_ADRES }}</p>
<!-- Спойлер с банковскими реквизитами -->
<details style="margin-top: 1.5rem;">
<summary style="cursor: pointer; font-weight: bold; color: var(--primary);">
🔒 Банковские реквизиты (нажмите, чтобы показать)
</summary>
<div style="margin-top: 1rem; padding-top: 1rem; border-top: 1px dashed var(--border-light);">
<p><strong>Расчётный счёт:</strong> {{ RQ_ECENT }}</p>
<p><strong>Банк:</strong> {{ RQ_BANK }}</p>
<p><strong>БИК:</strong> {{ RQ_BIK }}</p>
<p><strong>Корреспондентский счёт:</strong> {{ RQ_KOR_ECENT }}</p>
</div>
</details>
<p style="margin-top: 1rem;"><strong>Контактный телефон:</strong> {{ CONTACT_PHONE }}</p>
<p><strong>Электронная почта:</strong> {{ CONTACT_EMAIL }}</p>
</div>
<p class="mt-3 text-muted" style="color: var(--text-light);">
Актуально на {% now "d.m.Y" %}
</p>
</div>
</div>
{% endblock %}

View File

@ -1,29 +1,21 @@
{% extends 'programmer/base.html' %}
{% load django_bootstrap5 %}
{% load static %}
{% load seo_tags %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'programmer/css/solution-accordion.css' %}">
<link rel="stylesheet" href="{% static 'programmer/css/solution-cards.css' %}">
{% endblock %}
{% block content %}
<article>
<h1>{{ solution.title }}</h1>
<p>{{ solution.description }}</p>
<section>{{ solution.implementation|safe }}</section>
<footer>{{ solution.closing|safe }}</footer>
</article>
<div class="page-header">
<h1 class="page-title">Проекты автоматизации 1С</h1>
<p class="page-subtitle">Реализованные решения и кейсы по автоматизации бизнес-процессов</p>
</div>
<ul class="improved-list" id="projects-container">
<div class="projects-grid" id="projects-container">
{% include 'programmer/includes/project_cards.html' %}
</ul>
</div>
{% if not posts %}
<div class="content-card text-center">
@ -32,15 +24,15 @@
</div>
{% endif %}
<!-- Индикатор загрузки -->
{% if page_obj.has_next %}
<div id="loading-spinner" style="display: none; text-align: center; margin: 20px 0;">
<div class="spinner-border text-primary" role="status"><span class="visually-hidden">Загрузка...</span></div>
<p>Загрузка проектов...</p>
<div id="loading-spinner" style="display: none; text-align: center; margin: 2rem 0;">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
<p style="margin-top: 0.5rem; color: var(--text-secondary);">Загрузка проектов...</p>
</div>
{% endif %}
<!-- Данные пагинации -->
<script>
window.currentPage = {{ page_obj.number }};
window.totalPages = {{ paginator.num_pages }};
@ -49,7 +41,6 @@
{% endblock %}
{% block extra_js %}
<script src="{% static 'programmer/js/solution-accordion.js' %}"></script>
<script src="{% static 'programmer/js/infinite_scroll.js' %}"></script>
<script src="{% static 'programmer/js/floating-button.js' %}"></script>
<script src="{% static 'programmer/js/infinite_scroll.js' %}"></script>
<script src="{% static 'programmer/js/floating-button.js' %}"></script>
{% endblock %}

View File

@ -0,0 +1,109 @@
{% extends 'programmer/base.html' %}
{% load static %}
{% load seo_tags %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'programmer/css/solution-cards.css' %}">
{% endblock %}
{% block content %}
<div class="container">
{# Хлебные крошки #}
{% if breadcrumbs %}
<nav class="breadcrumbs" aria-label="Навигация">
{% for crumb in breadcrumbs %}
{% if crumb.url_name %}
<a href="{% url crumb.url_name %}" class="breadcrumb-link">{{ crumb.title }}</a>
<span class="breadcrumb-separator">/</span>
{% else %}
<span class="breadcrumb-current">{{ crumb.title }}</span>
{% endif %}
{% endfor %}
</nav>
{% endif %}
{# Заголовок страницы #}
<div class="page-header">
<h1 class="page-title">{{ solution.title }}</h1>
<p class="page-subtitle">
Опубликовано: {{ solution.time_create|date:"d.m.Y" }}
</p>
</div>
{# Основное содержимое #}
<article class="content-card">
{# Секция: Описание задачи #}
<section class="solution-section">
<h2 class="section-title">📋 Описание задачи</h2>
<div class="section-content">
{{ solution.description|safe }}
</div>
</section>
{# Секция: Описание решения #}
<section class="solution-section">
<h2 class="section-title">🔧 Описание решения</h2>
<div class="section-content">
{{ solution.implementation|safe }}
</div>
</section>
{# Секция: Результат #}
<section class="solution-section">
<h2 class="section-title">✅ Результат</h2>
<div class="section-content">
{{ solution.closing|safe }}
</div>
</section>
{# Кнопка возврата #}
<div style="margin-top: 2rem; text-align: center;">
<a href="{% url 'solution' %}" class="btn btn-outline">
← Вернуться к списку проектов
</a>
</div>
</article>
</div>
{% endblock %}
{% block extra_js %}
<script>
function toggleAccordion(header) {
const content = header.nextElementSibling;
const icon = header.querySelector('.accordion-icon');
// Закрываем все другие открытые элементы в этом аккордеоне (опционально)
const accordion = header.closest('.solution-accordion');
if (accordion) {
accordion.querySelectorAll('.accordion-header').forEach(otherHeader => {
if (otherHeader !== header) {
const otherContent = otherHeader.nextElementSibling;
const otherIcon = otherHeader.querySelector('.accordion-icon');
otherContent.classList.remove('active');
otherHeader.classList.remove('active');
if (otherIcon) otherIcon.style.transform = 'rotate(0deg)';
}
});
}
// Переключаем текущий элемент
content.classList.toggle('active');
header.classList.toggle('active');
if (content.classList.contains('active')) {
icon.style.transform = 'rotate(180deg)';
} else {
icon.style.transform = 'rotate(0deg)';
}
}
// Инициализация: первый элемент открыт по умолчанию
document.addEventListener('DOMContentLoaded', function() {
const firstHeader = document.querySelector('.accordion-header');
if (firstHeader) {
toggleAccordion(firstHeader);
}
});
</script>
<script src="{% static 'programmer/js/floating-button.js' %}"></script>
{% endblock %}

View File

@ -14,7 +14,7 @@ urlpatterns = [
path('', views.HomePageView.as_view(), name='home'),
path('about/', views.AboutPageView.as_view(), name='about'),
path('solutions/', views.SolutionListView.as_view(), name='solution'),
path('solutions/<int:pk>/', SolutionDetailView.as_view(), name='solution_detail'),
path('solutions/<slug:slug>/', SolutionDetailView.as_view(), name='solution_detail'),
# path('competence/', ability, name='ability'),
# path('competence/<int:pk>/', CompetenceDetailView.as_view(), name='competence_detail'),
path('recall/', views.RecallListView.as_view(), name='recall'),
@ -34,6 +34,7 @@ urlpatterns = [
path('profile/edit/', views.ProfileEditView.as_view(), name='profile_edit'),
path('privacy/', PrivacyPolicyView.as_view(), name='privacy'),
path('yandex_cdc16c33291495b9.html/', yandex_html, name='yandex_cdc16c33291495b9'),
path('requisites/', RequisitesPageView.as_view(), name='requisites'),
]

View File

@ -19,6 +19,7 @@ from .mixins import PageViewTrackingMixin, MenuContextMixin, BreadcrumbMixin
from .services import get_published_queryset, track_page_view
from typing import Any, Dict, Type
from django.template.loader import render_to_string
import os
class BasePageView(PageViewTrackingMixin, MenuContextMixin, TemplateView):
@ -80,7 +81,7 @@ class HomePageView(BasePageView):
'posts': posts,
'form': form,
'title': "Программист 1С Николай Сердюк - разработка и сопровождение",
'title': "SNA Tecnology - разработка и сопровождение",
'meta_description': (
"Профессиональный программист 1С с более чем 10-летним опытом. "
"Разработка, доработка, обновление и интеграция систем 1С. Сопровождение 1С."
@ -98,7 +99,7 @@ class AboutPageView(BasePageView, BreadcrumbMixin):
template_name = 'programmer/about.html'
def get_breadcrumbs(self):
return [{'title': 'Обо мне', 'url_name': None}]
return [{'title': 'О нас', 'url_name': None}]
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
context = super().get_context_data(**kwargs)
@ -106,14 +107,15 @@ class AboutPageView(BasePageView, BreadcrumbMixin):
context.update({
'form': form,
'title': "Программист 1С Николай Сердюк - 10+ лет опыта | Услуги 1С",
'title': "С.Н.А. Технологии — Профессиональная разработка и сопровождение 1С",
'meta_description': (
"Николай Сердюк - сертифицированный программист 1С с 10+ лет опыта. "
"Специализация: обновление 1С, разработка под ключ, интеграция, миграция с 1С 7.7."
"Команда экспертов по автоматизации бизнеса на платформе 1С. "
"Разработка, доработка, интеграция и поддержка 1С для компаний любого масштаба. "
"Официальный договор, опыт более 10 лет."
),
'meta_keywords': (
"программист 1С Николай Сердюк, обновление 1С, разработка 1С под ключ, "
"интеграция 1С, сертифицированный 1С, миграция 1С 7.7"
"разработка 1С, сопровождение 1С, автоматизация бизнеса 1С, "
"интеграция 1С, программист 1С компания, СНА Технологии, обновление 1С, 1С под ключ"
),
})
return context
@ -229,6 +231,10 @@ class RegisterView(PageViewTrackingMixin, MenuContextMixin, SuccessMessageMixin,
return context
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request', None)
super().__init__(*args, **kwargs)
class ProfileEditView(LoginRequiredMixin, PageViewTrackingMixin, MenuContextMixin, BreadcrumbMixin, UpdateView):
model = Profile
@ -281,7 +287,7 @@ class PrivacyPolicyView(TemplateView, MenuContextMixin, BreadcrumbMixin):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({
'title': "Политика конфиденциальности | СНА Технологии",
'title': "Политика конфиденциальности | С.Н.А. Технологии",
})
return context
@ -343,6 +349,37 @@ class CompetenceDetailView(MenuContextMixin, BreadcrumbMixin, DetailView):
return context
class RequisitesPageView(BasePageView, BreadcrumbMixin):
"""Страница с реквизитами компании."""
template_name = 'programmer/requisites.html'
def get_breadcrumbs(self):
return [
{'title': 'О компании', 'url_name': 'about'},
{'title': 'Реквизиты', 'url_name': None}
]
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
context = super().get_context_data(**kwargs)
context.update({
'title': "Реквизиты ИП Сердюк Н.А. | С.Н.А. Технологии",
'meta_description': (
"Официальные реквизиты ИП Сердюк Николай Александрович. "
"ИНН, ОГРНИП, банковские реквизиты для выставления счетов и заключения договоров."
),
'meta_keywords': "реквизиты ИП, Сердюк Николай Александрович, СНА Технологии, ИНН, ОГРНИП",
'RQ_INN': os.getenv('RQ_INN', 'не указан'),
'RQ_OGRNIP': os.getenv('RQ_OGRNIP', 'не указан'),
'RQ_ADRES': os.getenv('RQ_ADRES', 'не указан'),
'RQ_ECENT': os.getenv('RQ_ECENT', 'не указан'),
'RQ_BANK': os.getenv('RQ_BANK', 'не указан'),
'RQ_BIK': os.getenv('RQ_BIK', 'не указан'),
'RQ_KOR_ECENT': os.getenv('RQ_KOR_ECENT', 'не указан'),
})
return context
@require_POST
def callback_request(request: HttpRequest) -> HttpResponse:
"""

39
scripts/update_readme.py Normal file
View File

@ -0,0 +1,39 @@
#!/usr/bin/env python3
import os
import re
README_PATH = "README.md"
START_MARKER = "<!-- BEGIN_STRUCTURE -->"
END_MARKER = "<!-- END_STRUCTURE -->"
def generate_structure():
"""Создаёт список файлов и папок в корне репозитория."""
items = []
for name in sorted(os.listdir(".")):
if name.startswith('.') or name == 'scripts':
continue
icon = "📁" if os.path.isdir(name) else "📄"
items.append(f"- {icon} {name}")
return "\n".join(items)
def update_readme():
with open(README_PATH, "r", encoding="utf-8") as f:
content = f.read()
new_block = generate_structure()
pattern = re.compile(
re.escape(START_MARKER) + r".*?" + re.escape(END_MARKER),
re.DOTALL
)
replacement = START_MARKER + "\n" + new_block + "\n" + END_MARKER
if pattern.search(content):
new_content = pattern.sub(replacement, content)
else:
new_content = content.strip() + "\n\n" + replacement + "\n"
with open(README_PATH, "w", encoding="utf-8") as f:
f.write(new_content)
if __name__ == "__main__":
update_readme()

View File

@ -0,0 +1,194 @@
/* ===== PROJECTS GRID ===== */
.projects-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
/* ===== PROJECT CARD ===== */
.project-card {
background: var(--bg-card);
border-radius: var(--radius-lg);
border: 1px solid var(--border-light);
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
transition: transform 0.25s ease, box-shadow 0.25s ease, border-color 0.25s ease;
}
.project-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
border-color: var(--primary-light);
}
/* Оранжевая полоска сверху — единственный декоративный слой */
.project-card__accent {
height: 3px;
background: var(--gradient-primary);
flex-shrink: 0;
}
.project-card__body {
padding: 1.5rem 1.5rem 1rem;
flex: 1;
}
.project-card__title {
font-size: 1.125rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 0.75rem;
line-height: 1.4;
}
.project-card__desc {
font-size: 0.95rem;
color: var(--text-secondary);
line-height: 1.65;
margin: 0;
}
.project-card__footer {
padding: 1rem 1.5rem 1.25rem;
border-top: 1px solid var(--border-light);
display: flex;
align-items: center;
}
.project-card__link {
display: inline-flex;
align-items: center;
gap: 0.4rem;
color: var(--primary);
font-weight: 600;
font-size: 0.9rem;
text-decoration: none;
transition: gap 0.2s ease, color 0.2s ease;
}
.project-card__link:hover {
color: var(--primary-dark);
gap: 0.65rem;
}
/* ===== MOBILE ===== */
@media (max-width: 768px) {
.projects-grid {
grid-template-columns: 1fr;
gap: 1rem;
}
/* На мобиле убираем hover-подъём — он не работает на touch */
.project-card:hover {
transform: none;
box-shadow: none;
}
/* Вместо этого — активное состояние при нажатии */
.project-card:active {
border-color: var(--primary);
}
.project-card__body {
padding: 1.25rem 1.25rem 0.75rem;
}
.project-card__footer {
padding: 0.75rem 1.25rem 1rem;
}
.project-card__title {
font-size: 1rem;
}
}
@media (max-width: 480px) {
.projects-grid {
gap: 0.75rem;
}
}
/* ===== СТИЛИ ДЛЯ СТРАНИЦЫ ДЕТАЛЬНОГО РЕШЕНИЯ ===== */
.solution-section {
margin-bottom: 2.5rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid var(--border-light);
}
.solution-section:last-of-type {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.section-title {
font-size: 1.75rem;
font-weight: 700;
margin-bottom: 1.5rem;
color: var(--primary);
display: flex;
align-items: center;
gap: 0.5rem;
border-left: 5px solid var(--primary);
padding-left: 1.25rem;
}
.section-content {
font-size: 1.05rem;
line-height: 1.7;
color: var(--text-secondary);
}
/* Вложенные заголовки */
.section-content h2,
.section-content h3 {
margin-top: 1.5em;
margin-bottom: 0.75em;
color: var(--text-primary);
font-weight: 600;
}
.section-content h2 {
font-size: 1.4rem;
border-bottom: 1px dashed var(--border-light);
padding-bottom: 0.5rem;
}
.section-content h3 {
font-size: 1.2rem;
}
.section-content ul,
.section-content ol {
margin: 1rem 0 1rem 1.5rem;
}
.section-content li {
margin-bottom: 0.5rem;
}
/* Адаптация для мобильных */
@media (max-width: 768px) {
.solution-section {
margin-bottom: 2rem;
padding-bottom: 1rem;
}
.section-title {
font-size: 1.5rem;
padding-left: 1rem;
border-left-width: 4px;
}
.section-content {
font-size: 1rem;
}
}
@media (max-width: 480px) {
.section-title {
font-size: 1.35rem;
}
}

View File

@ -125,7 +125,7 @@ body {
.nav {
display: flex;
align-items: center;
justify-content: space-between;
justify-content: flex-start;
padding: 1rem 0;
gap: 2rem;
}
@ -160,16 +160,190 @@ body {
.nav-menu {
display: flex;
align-items: center;
list-style: none;
gap: 2.5rem;
margin: 0;
flex-wrap: wrap;
}
/* ===== DROPDOWN MENU (DESKTOP) ===== */
.nav-menu .has-dropdown {
position: relative;
}
/* Родительский пункт (span или ссылка) */
.nav-menu .has-dropdown > a,
.nav-menu .has-dropdown > span {
text-decoration: none;
color: var(--text-secondary);
font-weight: 600;
padding: 0.75rem 0;
position: relative;
font-size: 1rem;
display: inline-block;
cursor: default;
transition: var(--transition);
}
/* Выпадающий список */
.nav-menu .dropdown {
position: absolute;
top: 100%;
left: 0;
min-width: 200px;
background: var(--bg-card);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-xl);
border: 1px solid var(--border-light);
opacity: 0;
visibility: hidden;
transform: translateY(-10px);
transition: opacity 0.2s ease, visibility 0.2s ease, transform 0.2s ease;
list-style: none;
padding: 0.5rem 0;
z-index: 1000;
}
/* Показываем dropdown при наведении на родительский li */
.nav-menu .has-dropdown:hover .dropdown {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
/* Элементы dropdown */
.nav-menu .dropdown li {
margin: 0;
}
.nav-menu .dropdown a {
display: block;
padding: 0.6rem 1.2rem;
color: var(--text-primary);
text-decoration: none;
transition: var(--transition);
font-weight: 500;
white-space: nowrap;
}
.nav-menu .dropdown a:hover {
background: var(--bg-secondary);
color: var(--primary);
padding-left: 1.5rem;
}
/* Небольшой треугольник (опционально) */
.nav-menu .has-dropdown > span::after,
.nav-menu .has-dropdown > a::after {
/* content: '▼'; */
font-size: 0.75em;
margin-left: 6px;
opacity: 0.7;
vertical-align: middle;
transition: transform 0.2s ease;
}
.nav-menu .has-dropdown:hover > span::after,
.nav-menu .has-dropdown:hover > a::after {
transform: rotate(180deg);
}
/* ===== USER MENU DROPDOWN (DESKTOP) ===== */
.user-menu {
display: flex;
align-items: center;
gap: 0.5rem;
}
.user-menu-item {
position: relative;
}
.user-menu-parent {
cursor: default;
display: inline-block;
padding: 0.5rem 1rem; /* соответствие .nav-link */
}
.user-menu .has-dropdown > a,
.user-menu .has-dropdown > span {
cursor: default;
}
.user-menu .dropdown {
position: absolute;
top: 100%;
right: 0; /* выравнивание по правому краю */
min-width: 180px;
background: var(--bg-card);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-xl);
border: 1px solid var(--border-light);
opacity: 0;
visibility: hidden;
transform: translateY(-10px);
transition: opacity 0.2s ease, visibility 0.2s ease, transform 0.2s ease;
list-style: none;
padding: 0.5rem 0;
z-index: 1001;
}
.user-menu .has-dropdown:hover .dropdown {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.user-menu .dropdown li {
margin: 0;
}
.user-menu .dropdown-link,
.user-menu .dropdown-link-btn {
display: block;
width: 100%;
padding: 0.6rem 1.2rem;
color: var(--text-primary);
text-decoration: none;
transition: var(--transition);
font-weight: 500;
white-space: nowrap;
background: none;
border: none;
text-align: left;
font: inherit;
cursor: pointer;
}
.user-menu .dropdown-link:hover,
.user-menu .dropdown-link-btn:hover {
background: var(--bg-secondary);
color: var(--primary);
padding-left: 1.5rem;
}
/* Стрелка для родительского пункта (опционально) */
.user-menu .has-dropdown > span::after,
.user-menu .has-dropdown > a::after {
/* content: '▼'; */
font-size: 0.75em;
margin-left: 6px;
opacity: 0.7;
vertical-align: middle;
transition: transform 0.2s ease;
}
.user-menu .has-dropdown:hover > span::after,
.user-menu .has-dropdown:hover > a::after {
transform: rotate(180deg);
}
.nav-actions {
display: flex;
align-items: center;
gap: 1rem;
margin-left: auto;
}
.nav-link {
@ -1346,6 +1520,53 @@ body {
margin-bottom: 0.5rem;
}
/* ===== MOBILE SUBMENU ===== */
.mobile-nav-item.has-submenu {
position: relative;
}
/* Родительский пункт в мобильном меню (может быть span или ссылка) */
.mobile-nav-parent {
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
}
/* Стрелочка для родительского пункта */
.mobile-nav-parent::after {
content: '▼';
font-size: 0.8rem;
opacity: 0.7;
transition: transform 0.2s ease;
}
.has-submenu.submenu-open .mobile-nav-parent::after {
transform: rotate(180deg);
}
/* Вложенный список */
.mobile-submenu {
list-style: none;
margin-left: 1rem;
padding-left: 0.5rem;
border-left: 2px solid var(--primary);
display: none;
}
.has-submenu.submenu-open .mobile-submenu {
display: block;
}
.mobile-submenu .mobile-nav-item {
margin-bottom: 0;
}
.mobile-submenu .mobile-nav-link {
padding: 0.75rem 1rem;
font-size: 0.95rem;
}
.mobile-nav-link {
display: block;
padding: 1rem;
@ -1753,3 +1974,38 @@ body {
animation: pulse 1.5s infinite;
will-change: transform;
}
/* Стили для <details> / <summary> */
details {
margin: 1rem 0;
}
summary {
display: inline-block;
padding: 0.5rem 1rem;
background: var(--bg-primary);
border: 1px solid var(--border-light);
border-radius: var(--radius-md);
transition: var(--transition);
user-select: none;
}
summary:hover {
background: var(--primary);
color: white;
border-color: var(--primary);
}
details[open] summary {
background: var(--primary);
color: white;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
details[open] {
border: 1px solid var(--border-light);
border-radius: var(--radius-md);
padding: 1rem;
background: var(--bg-primary);
}

View File

@ -49,9 +49,14 @@ document.addEventListener('DOMContentLoaded', function() {
}
// Закрытие меню при клике на ссылку
const mobileNavLinks = document.querySelectorAll('.mobile-nav-link');
mobileNavLinks.forEach(link => {
link.addEventListener('click', closeMobileMenu);
document.querySelectorAll('.mobile-nav-item.has-submenu > .mobile-nav-link, .mobile-nav-item.has-submenu > .mobile-nav-parent').forEach(item => {
item.addEventListener('click', function(e) {
if (window.innerWidth <= 768) { // или ваш брейкпоинт
e.preventDefault();
const parent = this.closest('.has-submenu');
parent.classList.toggle('submenu-open');
}
});
});
// Синхронизация переключателей темы