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 migrate
python manage.py collectstatic python manage.py collectstatic
python manage.py createsuperuser python manage.py createsuperuser
## Структура проекта (автообновление)
<!-- BEGIN_STRUCTURE -->
<!-- END_STRUCTURE -->

View File

@ -60,15 +60,12 @@
{% 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="closeModal()">&times;</button> <button class="modal-close" onclick="ImageModal.close()">&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="">
@ -76,6 +73,9 @@
</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
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.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.contrib import messages from django.db import models
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
@ -11,38 +12,134 @@ 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):
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') 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):
@ -54,16 +151,12 @@ 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} заявок отмечены как прочитанные')
@ -85,8 +178,6 @@ 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 = [
@ -117,11 +208,8 @@ 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):
@ -134,9 +222,8 @@ 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
def has_change_permission(self, request, obj=None): def has_change_permission(self, request, obj=None):
return False return False

View File

@ -78,6 +78,7 @@ 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,23 +24,41 @@ class MenuContextMixin(ContextMixin):
# Основное меню (всегда) # Основное меню (всегда)
main_menu = [ main_menu = [
{'title': "Главная", 'url_name': 'home'}, # {'title': "Главная", 'url_name': 'home'},
{'title': "Проекты", 'url_name': 'solution'}, {
{'title': "Статьи", 'url_name': 'blog'}, 'title': "Разработки",
'url_name': None,
'children': [
{'title': "Кейсы", 'url_name': 'solution'},
{'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': 'profile'}, {
{'title': "Выйти", 'url_name': 'logout'}, # будет обработан как форма 'title': "Профиль",
'url_name': None,
'children': [
{'title': "Профиль", 'url_name': 'profile'},
{'title': "Выйти", 'url_name': 'logout'},
]
}
] ]
else: else:
user_menu = [ user_menu = [
{'title': "Войти", 'url_name': 'login'}, {
{'title': "Регистрация", 'url_name': 'register'}, 'title': "Войти",
'url_name': None,
'children': [
{'title': "Войти", 'url_name': 'login'},
{'title': "Регистрация", 'url_name': 'register'},
]
}
] ]
context['main_menu'] = main_menu context['main_menu'] = main_menu

View File

@ -1,3 +1,5 @@
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
@ -6,12 +8,15 @@ 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/", 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_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='Опубликован')
@ -40,7 +45,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/", 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_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='Опубликован')
@ -65,12 +70,23 @@ 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={'pk': self.pk}) return reverse('solution_detail', kwargs={'slug': self.slug})
class Meta: class Meta:
verbose_name = 'Проекты' verbose_name = 'Проекты'
@ -78,18 +94,17 @@ 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):
"""Генерирует SEO-описание для проекта""" # Используем closing если есть, иначе description
if self.description: source = self.closing or self.description
clean_desc = self.description[:160].replace('\n', ' ').strip() if source:
return f"Проект автоматизации: {self.title}. {clean_desc}..." clean = source[:160].replace('\n', ' ').strip()
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
@ -98,7 +113,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/", 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_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='Опубликован')
@ -118,8 +133,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='Прочитано')
@ -134,16 +149,18 @@ 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:
# Отправляем email уведомление try:
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)
# Сохраняем без повторного вызова сигнала except Exception:
sender.objects.filter(pk=instance.pk).update(notification_sent=True) # Логируем ошибку, но не прерываем сохранение объекта
logger.exception(
'Не удалось отправить email-уведомление для заявки pk=%s', instance.pk
)
class PageView(models.Model): class PageView(models.Model):
@ -190,8 +207,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)
@ -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=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,10 +1,11 @@
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
from typing import Type logger = logging.getLogger(__name__)
from django.db.models import Model, QuerySet
def get_client_ip(request: HttpRequest) -> str: def get_client_ip(request: HttpRequest) -> str:
@ -22,13 +23,11 @@ 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',
@ -48,7 +47,6 @@ 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),
@ -56,13 +54,12 @@ 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(),
} }
) )
@ -71,26 +68,14 @@ def track_page_view(request: HttpRequest) -> None:
visitor.visit_count += 1 visitor.visit_count += 1
visitor.save() visitor.save()
except Exception as e: except Exception:
# В продакшене лучше использовать логирование 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

@ -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 { .nav {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: flex-start;
padding: 1rem 0; padding: 1rem 0;
gap: 2rem; gap: 2rem;
} }
@ -160,16 +160,190 @@ 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 {
@ -1346,6 +1520,53 @@ 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;
@ -1752,4 +1973,39 @@ body {
.floating-btn .btn.pulse { .floating-btn .btn.pulse {
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,11 +49,16 @@ document.addEventListener('DOMContentLoaded', function() {
} }
// Закрытие меню при клике на ссылку // Закрытие меню при клике на ссылку
const mobileNavLinks = document.querySelectorAll('.mobile-nav-link'); document.querySelectorAll('.mobile-nav-item.has-submenu > .mobile-nav-link, .mobile-nav-item.has-submenu > .mobile-nav-parent').forEach(item => {
mobileNavLinks.forEach(link => { item.addEventListener('click', function(e) {
link.addEventListener('click', closeMobileMenu); if (window.innerWidth <= 768) { // или ваш брейкпоинт
e.preventDefault();
const parent = this.closest('.has-submenu');
parent.classList.toggle('submenu-open');
}
});
}); });
// Синхронизация переключателей темы // Синхронизация переключателей темы
function syncThemeToggles() { function syncThemeToggles() {
if (mobileThemeToggle && mainThemeToggle) { if (mobileThemeToggle && mainThemeToggle) {

View File

@ -4,88 +4,133 @@
{% 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С с более чем 10-летним опытом</p> <p class="page-subtitle">Команда экспертов по автоматизации бизнеса на платформе 1С</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="skill-category">
<h4>💻 Разработка</h4>
<ul>
<li>Разработка и доработка конфигураций 1С</li>
<li>Создание внешних обработок и отчетов</li>
<li>Кастомизация под бизнес-процессы</li>
</ul>
</div>
<div class="skill-category">
<h4>🔗 Интеграция</h4>
<ul>
<li>Интеграция 1С с веб-сервисами</li>
<li>Связь с сайтами и мобильными приложениями</li>
<li>API и веб-сервисы</li>
</ul>
</div>
<div class="skill-category">
<h4>⚡ Оптимизация</h4>
<ul>
<li>Оптимизация бизнес-процессов</li>
<li>Ускорение работы баз данных</li>
<li>Автоматизация рутинных операций</li>
</ul>
</div>
</div>
</div>
</div>
<div class="about-section">
<h3>🛠 Технологии и навыки</h3>
<div class="skills-grid"> <div class="skills-grid">
<div class="skill-category"> <div class="skill-category">
<h4>🎯 1С Разработка</h4> <h4>Николай Сердюк</h4>
<ul> <p style="margin-bottom: 0.5rem;">Руководитель проектов, ведущий архитектор 1С</p>
<li>1С:Предприятие 8.3</li> <ul style="margin-top: 0;">
<li>Управление торговлей</li> <li>10+ лет в разработке 1С</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>SQL и оптимизация запросов</li> <li>Аналитики бизнес-процессов</li>
<li>Веб-сервисы и API</li> <li>Специалисты по интеграции с внешними системами</li>
<li>XML, JSON, REST</li> <!-- <li>Инженеры технической поддержки 24/7</li> -->
<li>Системное администрирование</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>Настройка производительности серверов 1С</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> <div class="skills-grid">
<div class="skill-category">
<h4>🎯 Платформа 1С</h4>
<ul>
<li>1С:Предприятие 8.3</li>
<li>Управление торговлей (УТ)</li>
<li>Бухгалтерия предприятия (БП)</li>
<li>Зарплата и управление персоналом (ЗУП)</li>
<li>Управление небольшой фирмой (УНФ)</li>
</ul>
</div>
<div class="skill-category">
<h4>🔧 Смежные технологии</h4>
<ul>
<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>
<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>Интеграция 1С с сайтами и мобильными приложениями</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> </ul>
</div> </div>
</div> </div>
@ -93,58 +138,87 @@
<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> <p><strong>{{ CONTACT_EMAIL }}</strong></p>
<p><strong>{{ CONTACT_EMAIL }}</strong></p> </div>
</div> <div class="skill-category">
<div class="skill-category"> <h4>📱 Телефон</h4>
<h4>📱 Телефон</h4> <p><strong>{{ CONTACT_PHONE }}</strong></p>
<p><strong>{{ CONTACT_PHONE }}</strong></p> </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": "Service", "@type": "ProfessionalService",
"serviceType": "1С программирование", "name": "С.Н.А. Технологии",
"provider": { "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", "@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С"
} }
} }
] ]
@ -154,5 +228,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,20 +122,29 @@
<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="nav-item"> <li class="{% if item.children %}has-dropdown{% endif %}">
<a href="{% url item.url_name %}" class="nav-link {% if request.resolver_match.url_name == item.url_name %}active{% endif %}"> {% if item.url_name %}
{{ item.title }} <a href="{% url item.url_name %}" class="nav-link">{{ item.title }}</a>
</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> </li>
{% endfor %} {% endfor %}
</ul> </ul>
<div class="nav-actions"> <div class="nav-actions">
<!-- Telegram --> <!-- Telegram -->
@ -155,31 +164,49 @@
</label> </label>
</div> </div>
<div class="user-menu"> <div class="user-menu">
{% for item in user_menu %} {% for item in user_menu %}
{% if item.url_name == 'logout' %} <div class="user-menu-item {% if item.children %}has-dropdown{% endif %}">
<form method="post" action="{% url 'logout' %}" style="display: inline;"> {% if item.url_name %}
{% csrf_token %} <a href="{% url item.url_name %}" class="nav-link {% if request.resolver_match.url_name == item.url_name %}active{% endif %}">
<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 }} {{ item.title }}
</button> </a>
</form> {% else %}
{% elif item.url_name == 'login' %} <span class="nav-link user-menu-parent">{{ item.title }}</span>
<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;"> {% endif %}
{{ item.title }}
</a> {% if item.children %}
{% elif item.url_name == 'register' %} <ul class="dropdown user-dropdown">
<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;"> {% for child in item.children %}
{{ item.title }} <li>
</a> {% if child.url_name == 'logout' %}
{% else %} <form method="post" action="{% url 'logout' %}" style="display: block;">
<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;"> {% csrf_token %}
{{ item.title }} <button type="submit" class="dropdown-link-btn">
</a> {{ child.title }}
{% endif %} </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 %} {% endfor %}
</div> </div>
<!-- Кнопка мобильного меню --> <!-- Кнопка мобильного меню -->
<button class="mobile-menu-btn" id="mobileMenuBtn"> <button class="mobile-menu-btn" id="mobileMenuBtn">
@ -201,37 +228,70 @@
<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"> <li class="mobile-nav-item {% if m.children %}has-submenu{% endif %}">
{% if m.url_name == 'logout' %} {% if m.url_name %}
<form method="post" action="{% url 'logout' %}" style="display: block;"> <a href="{% url m.url_name %}" class="mobile-nav-link {% if request.resolver_match.url_name == m.url_name %}active{% endif %}">
{% 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 }} {{ m.title }}
</button> </a>
</form> {% else %}
{% else %} <span class="mobile-nav-link mobile-nav-parent">{{ m.title }}</span>
<a href="{% url m.url_name %}" class="mobile-nav-link {% if request.resolver_match.url_name == m.url_name %}active{% endif %}"> {% endif %}
{{ m.title }}
</a> {% if m.children %}
{% endif %} <ul class="mobile-submenu">
</li> {% 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 %} {% endfor %}
{% for item in user_menu %} {% for item in user_menu %}
{% if item.url_name == 'logout' %} <li class="mobile-nav-item {% if item.children %}has-submenu{% endif %}">
<form method="post" action="{% url 'logout' %}" style="display: inline;"> {% if item.url_name %}
{% csrf_token %} <a href="{% url item.url_name %}" class="mobile-nav-link {% if request.resolver_match.url_name == item.url_name %}active{% endif %}">
<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 }} {{ item.title }}
</button> </a>
</form> {% else %}
{% else %} <span class="mobile-nav-link mobile-nav-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 }}
</a> {% if item.children %}
{% endif %} <ul class="mobile-submenu">
{% endfor %} {% for child in item.children %}
</ul> <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>
<div class="mobile-nav-actions"> <div class="mobile-nav-actions">
<!-- <a href="https://t.me/odinesina_prog" target="_blank" class="btn btn-primary" style="width: 100%; text-align: center;">--> <!-- <a href="https://t.me/odinesina_prog" target="_blank" class="btn btn-primary" style="width: 100%; text-align: center;">-->

View File

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

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,6 +30,8 @@
{% 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

@ -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' %} {% 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-accordion.css' %}"> <link rel="stylesheet" href="{% static 'programmer/css/solution-cards.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>
<ul class="improved-list" id="projects-container"> <div class="projects-grid" id="projects-container">
{% include 'programmer/includes/project_cards.html' %} {% include 'programmer/includes/project_cards.html' %}
</ul> </div>
{% if not posts %} {% if not posts %}
<div class="content-card text-center"> <div class="content-card text-center">
@ -32,15 +24,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: 20px 0;"> <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> <div class="spinner-border text-primary" role="status">
<p>Загрузка проектов...</p> <span class="visually-hidden">Загрузка...</span>
</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 }};
@ -49,7 +41,6 @@
{% endblock %} {% endblock %}
{% block extra_js %} {% 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/infinite_scroll.js' %}"></script> <script src="{% static 'programmer/js/floating-button.js' %}"></script>
<script src="{% static 'programmer/js/floating-button.js' %}"></script>
{% endblock %} {% 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('', 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/<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/', 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,6 +34,7 @@ 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,6 +19,7 @@ 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):
@ -80,7 +81,7 @@ class HomePageView(BasePageView):
'posts': posts, 'posts': posts,
'form': form, 'form': form,
'title': "Программист 1С Николай Сердюк - разработка и сопровождение", 'title': "SNA Tecnology - разработка и сопровождение",
'meta_description': ( 'meta_description': (
"Профессиональный программист 1С с более чем 10-летним опытом. " "Профессиональный программист 1С с более чем 10-летним опытом. "
"Разработка, доработка, обновление и интеграция систем 1С. Сопровождение 1С." "Разработка, доработка, обновление и интеграция систем 1С. Сопровождение 1С."
@ -98,7 +99,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)
@ -106,14 +107,15 @@ class AboutPageView(BasePageView, BreadcrumbMixin):
context.update({ context.update({
'form': form, 'form': form,
'title': "Программист 1С Николай Сердюк - 10+ лет опыта | Услуги 1С", 'title': "С.Н.А. Технологии — Профессиональная разработка и сопровождение 1С",
'meta_description': ( 'meta_description': (
"Николай Сердюк - сертифицированный программист 1С с 10+ лет опыта. " "Команда экспертов по автоматизации бизнеса на платформе 1С. "
"Специализация: обновление 1С, разработка под ключ, интеграция, миграция с 1С 7.7." "Разработка, доработка, интеграция и поддержка 1С для компаний любого масштаба. "
"Официальный договор, опыт более 10 лет."
), ),
'meta_keywords': ( 'meta_keywords': (
"программист 1С Николай Сердюк, обновление 1С, разработка 1С под ключ, " "разработка 1С, сопровождение 1С, автоматизация бизнеса 1С, "
"интеграция 1С, сертифицированный 1С, миграция 1С 7.7" "интеграция 1С, программист 1С компания, СНА Технологии, обновление 1С, 1С под ключ"
), ),
}) })
return context return context
@ -228,6 +230,10 @@ 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):
@ -281,7 +287,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
@ -343,6 +349,37 @@ 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:
""" """

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 { .nav {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: flex-start;
padding: 1rem 0; padding: 1rem 0;
gap: 2rem; gap: 2rem;
} }
@ -160,16 +160,190 @@ 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 {
@ -1346,6 +1520,53 @@ 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;
@ -1752,4 +1973,39 @@ body {
.floating-btn .btn.pulse { .floating-btn .btn.pulse {
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,11 +49,16 @@ document.addEventListener('DOMContentLoaded', function() {
} }
// Закрытие меню при клике на ссылку // Закрытие меню при клике на ссылку
const mobileNavLinks = document.querySelectorAll('.mobile-nav-link'); document.querySelectorAll('.mobile-nav-item.has-submenu > .mobile-nav-link, .mobile-nav-item.has-submenu > .mobile-nav-parent').forEach(item => {
mobileNavLinks.forEach(link => { item.addEventListener('click', function(e) {
link.addEventListener('click', closeMobileMenu); if (window.innerWidth <= 768) { // или ваш брейкпоинт
e.preventDefault();
const parent = this.closest('.has-submenu');
parent.classList.toggle('submenu-open');
}
});
}); });
// Синхронизация переключателей темы // Синхронизация переключателей темы
function syncThemeToggles() { function syncThemeToggles() {
if (mobileThemeToggle && mainThemeToggle) { if (mobileThemeToggle && mainThemeToggle) {