Compare commits

..

No commits in common. "main" and "first" have entirely different histories.
main ... first

25 changed files with 256 additions and 1687 deletions

View File

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

View File

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

View File

@ -3,8 +3,7 @@ from django.utils import timezone
from django.utils.html import format_html from django.utils.html import format_html
from django.urls import path from django.urls import path
from django.shortcuts import render from django.shortcuts import render
from django.db import models from django.contrib import messages
from django_ckeditor_5.widgets import CKEditor5Widget
from .models import Home, Solution, Competence, Recall, CallbackRequest, PageView from .models import Home, Solution, Competence, Recall, CallbackRequest, PageView
@ -12,134 +11,38 @@ from .models import Home, Solution, Competence, Recall, CallbackRequest, PageVie
# ===== BASE ADMIN CLASSES ===== # ===== BASE ADMIN CLASSES =====
class BaseContentAdmin(admin.ModelAdmin): class BaseContentAdmin(admin.ModelAdmin):
""" """Общий базовый класс для контентных моделей."""
Базовый класс для контентных моделей.
CKEditor, actions публикации, date_hierarchy.
"""
list_display_links = ('id', 'title') list_display_links = ('id', 'title')
list_editable = ('is_published',) list_editable = ('is_published',)
list_filter = ('time_create', 'is_published') list_filter = ('time_create', 'is_published')
search_fields = ('title',) 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 ===== # ===== MODEL ADMINS =====
@admin.register(Home) @admin.register(Home)
class HomeAdmin(BaseContentAdmin): class HomeAdmin(BaseContentAdmin):
# Поля: title, content, home_image, time_create, time_update, is_published
list_display = ('id', 'title', 'time_create', 'is_published') list_display = ('id', 'title', 'time_create', 'is_published')
search_fields = ('title', 'content') 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) @admin.register(Solution)
class SolutionAdmin(BaseContentAdmin): class SolutionAdmin(BaseContentAdmin):
# Поля: title, description, implementation, closing, time_create, time_update, is_published
list_display = ('id', 'title', 'time_create', 'is_published') list_display = ('id', 'title', 'time_create', 'is_published')
list_display_links = ('id', 'title')
search_fields = ('title', 'description', 'implementation') 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) @admin.register(Competence)
class CompetenceAdmin(BaseContentAdmin): class CompetenceAdmin(BaseContentAdmin):
# Поля: title, content, photo, time_create, time_update, is_published
list_display = ('id', 'title', 'time_create', 'is_published') list_display = ('id', 'title', 'time_create', 'is_published')
search_fields = ('title', 'content') search_fields = ('title', 'content')
fieldsets = (
(None, {
'fields': ('title', 'is_published'),
}),
('Содержание', {
'fields': ('content', 'photo'),
'description': 'Описание компетенции и фото.',
}),
('Служебное', {
'fields': ('time_create', 'time_update'),
'classes': ('collapse',),
}),
)
@admin.register(Recall) @admin.register(Recall)
class RecallAdmin(BaseContentAdmin): class RecallAdmin(BaseContentAdmin):
# Поля: title, content, scan, time_create, time_update, is_published list_display = ('id', 'title', 'time_create', 'scan', 'is_published')
list_display = ('id', 'title', 'time_create', 'scan_preview', 'is_published')
search_fields = ('title', 'content') 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) @admin.register(CallbackRequest)
class CallbackAdmin(admin.ModelAdmin): class CallbackAdmin(admin.ModelAdmin):
@ -151,12 +54,16 @@ class CallbackAdmin(admin.ModelAdmin):
readonly_fields = ('time_create',) readonly_fields = ('time_create',)
actions = ['mark_as_read', 'mark_as_unread', 'mark_as_processed', 'resend_notification'] actions = ['mark_as_read', 'mark_as_unread', 'mark_as_processed', 'resend_notification']
# ── badge helper ──────────────────────────────────────────────────────────
def new_badge(self, obj): def new_badge(self, obj):
if not obj.is_read: if not obj.is_read:
return format_html('<span style="color:red;font-weight:bold;">🆕 НОВАЯ</span>') return format_html('<span style="color:red;font-weight:bold;">🆕 НОВАЯ</span>')
return '' return ''
new_badge.short_description = 'Статус' new_badge.short_description = 'Статус'
# ── bulk actions ──────────────────────────────────────────────────────────
def mark_as_read(self, request, queryset): def mark_as_read(self, request, queryset):
updated = queryset.update(is_read=True) updated = queryset.update(is_read=True)
self.message_user(request, f'{updated} заявок отмечены как прочитанные') self.message_user(request, f'{updated} заявок отмечены как прочитанные')
@ -178,6 +85,8 @@ class CallbackAdmin(admin.ModelAdmin):
self.message_user(request, f'Уведомления отправлены для {count} заявок') self.message_user(request, f'Уведомления отправлены для {count} заявок')
resend_notification.short_description = 'Переотправить email уведомления' resend_notification.short_description = 'Переотправить email уведомления'
# ── custom URL + view for stats ───────────────────────────────────────────
def get_urls(self): def get_urls(self):
urls = super().get_urls() urls = super().get_urls()
custom_urls = [ custom_urls = [
@ -208,8 +117,11 @@ class CallbackAdmin(admin.ModelAdmin):
} }
return render(request, 'admin/callback_stats.html', context) 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) @admin.register(PageView)
class PageViewAdmin(admin.ModelAdmin): class PageViewAdmin(admin.ModelAdmin):
@ -222,6 +134,7 @@ class PageViewAdmin(admin.ModelAdmin):
def get_queryset(self, request): def get_queryset(self, request):
return super().get_queryset(request).order_by('-timestamp') return super().get_queryset(request).order_by('-timestamp')
# PageViews should never be created or edited manually
def has_add_permission(self, request): def has_add_permission(self, request):
return False return False

View File

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

View File

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

View File

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

View File

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

View File

@ -1,194 +0,0 @@
/* ===== 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 { .nav {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-start; justify-content: space-between;
padding: 1rem 0; padding: 1rem 0;
gap: 2rem; gap: 2rem;
} }
@ -160,190 +160,16 @@ body {
.nav-menu { .nav-menu {
display: flex; display: flex;
align-items: center;
list-style: none; list-style: none;
gap: 2.5rem; gap: 2.5rem;
margin: 0; margin: 0;
flex-wrap: wrap; 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 { .nav-actions {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 1rem; gap: 1rem;
margin-left: auto;
} }
.nav-link { .nav-link {
@ -1520,53 +1346,6 @@ body {
margin-bottom: 0.5rem; 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 { .mobile-nav-link {
display: block; display: block;
padding: 1rem; padding: 1rem;
@ -1974,38 +1753,3 @@ body {
animation: pulse 1.5s infinite; animation: pulse 1.5s infinite;
will-change: transform; 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,14 +49,9 @@ document.addEventListener('DOMContentLoaded', function() {
} }
// Закрытие меню при клике на ссылку // Закрытие меню при клике на ссылку
document.querySelectorAll('.mobile-nav-item.has-submenu > .mobile-nav-link, .mobile-nav-item.has-submenu > .mobile-nav-parent').forEach(item => { const mobileNavLinks = document.querySelectorAll('.mobile-nav-link');
item.addEventListener('click', function(e) { mobileNavLinks.forEach(link => {
if (window.innerWidth <= 768) { // или ваш брейкпоинт link.addEventListener('click', closeMobileMenu);
e.preventDefault();
const parent = this.closest('.has-submenu');
parent.classList.toggle('submenu-open');
}
});
}); });
// Синхронизация переключателей темы // Синхронизация переключателей темы

View File

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

View File

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

View File

@ -1,25 +1,5 @@
{% for solution in posts %} {% autoescape off %}
<article class="project-card"> {% for post in posts %}
<div class="project-card__accent"></div> {% include 'programmer/includes/project_card.html' %}
<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 %} {% endfor %}
{% endautoescape %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,39 +0,0 @@
#!/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

@ -1,194 +0,0 @@
/* ===== 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 { .nav {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-start; justify-content: space-between;
padding: 1rem 0; padding: 1rem 0;
gap: 2rem; gap: 2rem;
} }
@ -160,190 +160,16 @@ body {
.nav-menu { .nav-menu {
display: flex; display: flex;
align-items: center;
list-style: none; list-style: none;
gap: 2.5rem; gap: 2.5rem;
margin: 0; margin: 0;
flex-wrap: wrap; 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 { .nav-actions {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 1rem; gap: 1rem;
margin-left: auto;
} }
.nav-link { .nav-link {
@ -1520,53 +1346,6 @@ body {
margin-bottom: 0.5rem; 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 { .mobile-nav-link {
display: block; display: block;
padding: 1rem; padding: 1rem;
@ -1974,38 +1753,3 @@ body {
animation: pulse 1.5s infinite; animation: pulse 1.5s infinite;
will-change: transform; 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,14 +49,9 @@ document.addEventListener('DOMContentLoaded', function() {
} }
// Закрытие меню при клике на ссылку // Закрытие меню при клике на ссылку
document.querySelectorAll('.mobile-nav-item.has-submenu > .mobile-nav-link, .mobile-nav-item.has-submenu > .mobile-nav-parent').forEach(item => { const mobileNavLinks = document.querySelectorAll('.mobile-nav-link');
item.addEventListener('click', function(e) { mobileNavLinks.forEach(link => {
if (window.innerWidth <= 768) { // или ваш брейкпоинт link.addEventListener('click', closeMobileMenu);
e.preventDefault();
const parent = this.closest('.has-submenu');
parent.classList.toggle('submenu-open');
}
});
}); });
// Синхронизация переключателей темы // Синхронизация переключателей темы