Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f4bef815be | |||
| 0134941e0f | |||
|
|
ed584ac3de | ||
|
|
f710301c00 | ||
|
|
4b671736ce | ||
| c9aac145cf | |||
|
|
412dfbffac | ||
| 6e0e367569 | |||
|
|
dd9be8feb5 | ||
| 9982c682c3 | |||
|
|
f72eb0246a | ||
| 4ac7705f71 | |||
|
|
59c09011ea | ||
|
|
70077ed279 | ||
|
|
9eed6b578b | ||
|
|
506ec6e2f9 | ||
|
|
53d97f674e | ||
|
|
d974bbd740 | ||
|
|
b9a6e2a95b | ||
| 8462c66f72 | |||
|
|
dffb41780a | ||
| dd99fdffe8 | |||
|
|
223fea5369 | ||
|
|
df0e27696c | ||
|
|
cf0d48c800 | ||
|
|
443d547f9d |
29
.gitea/workflows/update-readme.yml
Normal file
29
.gitea/workflows/update-readme.yml
Normal 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
|
||||||
@ -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 -->
|
||||||
@ -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()">×</button>
|
<button class="modal-close" onclick="ImageModal.close()">×</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 || 'Изображение из статьи');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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
|
||||||
@ -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,
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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')
|
|
||||||
@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
194
programmer/static/programmer/css/solution-cards.css
Normal file
194
programmer/static/programmer/css/solution-cards.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
}
|
}
|
||||||
@ -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) {
|
||||||
|
|||||||
@ -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 %}
|
||||||
|
|
||||||
@ -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;">-->
|
||||||
|
|||||||
@ -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 %}
|
||||||
@ -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">
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
50
programmer/templates/programmer/requisites.html
Normal file
50
programmer/templates/programmer/requisites.html
Normal 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 %}
|
||||||
@ -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 %}
|
||||||
109
programmer/templates/programmer/solution_detail.html
Normal file
109
programmer/templates/programmer/solution_detail.html
Normal 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 %}
|
||||||
@ -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'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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
39
scripts/update_readme.py
Normal 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()
|
||||||
194
static/programmer/css/solution-cards.css
Normal file
194
static/programmer/css/solution-cards.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
}
|
}
|
||||||
@ -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) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user