diff --git a/OneCprogsite/OneCprogsite/asgi.py b/OneCprogsite/OneCprogsite/asgi.py index 928b9fd..d008858 100644 --- a/OneCprogsite/OneCprogsite/asgi.py +++ b/OneCprogsite/OneCprogsite/asgi.py @@ -1,16 +1,16 @@ -""" -ASGI config for OneCprogsite project. - -It exposes the ASGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ -""" - -import os - -from django.core.asgi import get_asgi_application - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'OneCprogsite.settings') - -application = get_asgi_application() +""" +ASGI config for OneCprogsite project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'OneCprogsite.settings') + +application = get_asgi_application() diff --git a/OneCprogsite/OneCprogsite/settings.py b/OneCprogsite/OneCprogsite/settings.py index 640b35a..a02cb85 100644 --- a/OneCprogsite/OneCprogsite/settings.py +++ b/OneCprogsite/OneCprogsite/settings.py @@ -1,212 +1,212 @@ -""" -Django settings for OneCprogsite project. - -Generated by 'django-admin startproject' using Django 4.2.7. - -For more information on this file, see -https://docs.djangoproject.com/en/4.2/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/4.2/ref/settings/ -""" -import os.path -from pathlib import Path - -# Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent - - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'django-insecure-5rs2a1*8cxjkv*%6k1-88biv&1#nep%@i+%1^dk=5j$s&e&hwm' - -# Безопасность cookies для HTTPS -SESSION_COOKIE_SECURE = True -CSRF_COOKIE_SECURE = True -SESSION_COOKIE_HTTPONLY = True -CSRF_COOKIE_HTTPONLY = False # Django требует доступ к CSRF cookie через JS -SESSION_COOKIE_SAMESITE = 'Lax' -CSRF_COOKIE_SAMESITE = 'Lax' - -# Если используете другие cookies -LANGUAGE_COOKIE_SECURE = True -LANGUAGE_COOKIE_HTTPONLY = True -LANGUAGE_COOKIE_SAMESITE = 'Lax' - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = False - -X_FRAME_OPTIONS = 'SAMEORIGIN' -# Или разрешить конкретные домены (Django 4.0+) -X_FRAME_OPTIONS = 'ALLOW-FROM https://metrika.yandex.ru' - -# ОБЯЗАТЕЛЬНО укажите ваши домены -ALLOWED_HOSTS = [ - 'nikdizell.ru', - 'www.nikdizell.ru', - 'localhost', - '127.0.0.1', - '192.168.31.88' # Добавьте IP сервера -] - -# Важно для работы за прокси -SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') -SECURE_SSL_REDIRECT = True - -# Дополнительная безопасность -SECURE_BROWSER_XSS_FILTER = True -SECURE_CONTENT_TYPE_NOSNIFF = True -SECURE_HSTS_SECONDS = 31536000 -SECURE_HSTS_INCLUDE_SUBDOMAINS = True -SECURE_HSTS_PRELOAD = True - -CSRF_TRUSTED_ORIGINS = [ - 'https://nikdizell.ru', - 'https://www.nikdizell.ru', -] - -# Application definition - -INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'programmer.apps.ProgrammerConfig', - 'django_bootstrap5', - 'django_extensions', - 'django.contrib.sites', - 'django.contrib.sitemaps', -] - -MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'programmer.middleware.PageViewMiddleware', -] - -ROOT_URLCONF = 'OneCprogsite.urls' - -# Кастомный middleware для CSP -class CSPMiddleware: - def __init__(self, get_response): - self.get_response = get_response - - def __call__(self, request): - response = self.get_response(request) - response['Content-Security-Policy'] = "frame-ancestors 'self' https://metrika.yandex.ru https://metrika.yandex.by https://metrica.yandex.com https://metrica.yandex.com.tr https://*.webvisor.com" - return response - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - 'programmer.context_processors.menu_processor', - 'programmer.context_processors.contact_info', - ], - }, - }, -] - -WSGI_APPLICATION = 'OneCprogsite.wsgi.application' - - -# Database -# https://docs.djangoproject.com/en/4.2/ref/settings/#databases - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': 'App', - 'USER': 'postgres', - 'PASSWORD': 'NikDi94Zell', - 'HOST': 'postgres', - 'PORT': 5432, - } -} - - -# Password validation -# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators - -AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', - }, -] - - -# Internationalization -# https://docs.djangoproject.com/en/4.2/topics/i18n/ - -LANGUAGE_CODE = 'ru' - -TIME_ZONE = 'Europe/Moscow' -USE_I18N = True -USE_I18N = True -USE_TZ = True - - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/4.2/howto/static-files/ - -STATIC_URL = 'static/' -STATIC_ROOT = os.path.join(BASE_DIR, 'static') -STATICFILES_DIRS = [] - -# Default primary key field type -# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field - -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' - -MEDIA_ROOT = os.path.join(BASE_DIR, 'media') -MEDIA_URL = '/media/' - -# Настройки email -EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' -# EMAIL_HOST = 'smtp.yandex.ru' # или smtp.gmail.com, smtp.mail.ru -# EMAIL_PORT = 587 -# EMAIL_USE_TLS = True -# EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER', 'it@yandex.ru') -# EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD', 'tifdctkrcjcqwxyc') -# DEFAULT_FROM_EMAIL = EMAIL_HOST_USER -# SERVER_EMAIL = EMAIL_HOST_USER - -EMAIL_HOST = 'smtp.gmail.com' # или smtp.gmail.com, smtp.mail.ru -EMAIL_PORT = 587 -EMAIL_USE_TLS = True -EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER', 'nikdizell@gmail.com') -EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD', 'qvmw yccb msqv mmpj') -DEFAULT_FROM_EMAIL = EMAIL_HOST_USER -SERVER_EMAIL = EMAIL_HOST_USER - -# Email для уведомлений (можно указать несколько через запятую) -# ADMIN_EMAILS = os.getenv('ADMIN_EMAILS', 'nikdizell@gmail.com').split(',') -ADMIN_EMAILS = os.getenv('ADMIN_EMAILS', 'it@nserdyuk.ru').split(',') - - +""" +Django settings for OneCprogsite project. + +Generated by 'django-admin startproject' using Django 4.2.7. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.2/ref/settings/ +""" +import os.path +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-5rs2a1*8cxjkv*%6k1-88biv&1#nep%@i+%1^dk=5j$s&e&hwm' + +# Безопасность cookies для HTTPS +SESSION_COOKIE_SECURE = True +CSRF_COOKIE_SECURE = True +SESSION_COOKIE_HTTPONLY = True +CSRF_COOKIE_HTTPONLY = False # Django требует доступ к CSRF cookie через JS +SESSION_COOKIE_SAMESITE = 'Lax' +CSRF_COOKIE_SAMESITE = 'Lax' + +# Если используете другие cookies +LANGUAGE_COOKIE_SECURE = True +LANGUAGE_COOKIE_HTTPONLY = True +LANGUAGE_COOKIE_SAMESITE = 'Lax' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = False + +X_FRAME_OPTIONS = 'SAMEORIGIN' +# Или разрешить конкретные домены (Django 4.0+) +X_FRAME_OPTIONS = 'ALLOW-FROM https://metrika.yandex.ru' + +# ОБЯЗАТЕЛЬНО укажите ваши домены +ALLOWED_HOSTS = [ + 'nikdizell.ru', + 'www.nikdizell.ru', + 'localhost', + '127.0.0.1', + '192.168.31.88' # Добавьте IP сервера +] + +# Важно для работы за прокси +SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') +SECURE_SSL_REDIRECT = True + +# Дополнительная безопасность +SECURE_BROWSER_XSS_FILTER = True +SECURE_CONTENT_TYPE_NOSNIFF = True +SECURE_HSTS_SECONDS = 31536000 +SECURE_HSTS_INCLUDE_SUBDOMAINS = True +SECURE_HSTS_PRELOAD = True + +CSRF_TRUSTED_ORIGINS = [ + 'https://nikdizell.ru', + 'https://www.nikdizell.ru', +] + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'programmer.apps.ProgrammerConfig', + 'django_bootstrap5', + 'django_extensions', + 'django.contrib.sites', + 'django.contrib.sitemaps', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'programmer.middleware.PageViewMiddleware', +] + +ROOT_URLCONF = 'OneCprogsite.urls' + +# Кастомный middleware для CSP +class CSPMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + response = self.get_response(request) + response['Content-Security-Policy'] = "frame-ancestors 'self' https://metrika.yandex.ru https://metrika.yandex.by https://metrica.yandex.com https://metrica.yandex.com.tr https://*.webvisor.com" + return response + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + 'programmer.context_processors.menu_processor', + 'programmer.context_processors.contact_info', + ], + }, + }, +] + +WSGI_APPLICATION = 'OneCprogsite.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/4.2/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': 'App', + 'USER': 'postgres', + 'PASSWORD': 'NikDi94Zell', + 'HOST': 'postgres', + 'PORT': 5432, + } +} + + +# Password validation +# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/4.2/topics/i18n/ + +LANGUAGE_CODE = 'ru' + +TIME_ZONE = 'Europe/Moscow' +USE_I18N = True +USE_I18N = True +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.2/howto/static-files/ + +STATIC_URL = 'static/' +STATIC_ROOT = os.path.join(BASE_DIR, 'static') +STATICFILES_DIRS = [] + +# Default primary key field type +# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +MEDIA_ROOT = os.path.join(BASE_DIR, 'media') +MEDIA_URL = '/media/' + +# Настройки email +EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' +# EMAIL_HOST = 'smtp.yandex.ru' # или smtp.gmail.com, smtp.mail.ru +# EMAIL_PORT = 587 +# EMAIL_USE_TLS = True +# EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER', 'it@yandex.ru') +# EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD', 'tifdctkrcjcqwxyc') +# DEFAULT_FROM_EMAIL = EMAIL_HOST_USER +# SERVER_EMAIL = EMAIL_HOST_USER + +EMAIL_HOST = 'smtp.gmail.com' # или smtp.gmail.com, smtp.mail.ru +EMAIL_PORT = 587 +EMAIL_USE_TLS = True +EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER', 'nikdizell@gmail.com') +EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD', 'qvmw yccb msqv mmpj') +DEFAULT_FROM_EMAIL = EMAIL_HOST_USER +SERVER_EMAIL = EMAIL_HOST_USER + +# Email для уведомлений (можно указать несколько через запятую) +# ADMIN_EMAILS = os.getenv('ADMIN_EMAILS', 'nikdizell@gmail.com').split(',') +ADMIN_EMAILS = os.getenv('ADMIN_EMAILS', 'it@nserdyuk.ru').split(',') + + diff --git a/OneCprogsite/OneCprogsite/urls.py b/OneCprogsite/OneCprogsite/urls.py index 84253d8..fdecba6 100644 --- a/OneCprogsite/OneCprogsite/urls.py +++ b/OneCprogsite/OneCprogsite/urls.py @@ -1,39 +1,39 @@ -""" -URL configuration for OneCprogsite project. - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/4.2/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: path('', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.urls import include, path - 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) -""" -from django.conf.urls.static import static -from django.contrib import admin -from django.urls import path - -from OneCprogsite import settings -from programmer.views import * -from django.urls import path, include - -urlpatterns = [ - path('admin/', admin.site.urls), - path('', include('programmer.urls')), - # path('', index, name='home'), - # path('about/', about, name='about'), - # path('solution/', solution, name='solution'), - # path('ability/', ability, name='ability'), - # path('recall/', recall, name='recall'), - # path('post/', show_post, name='post'), -] - -if settings.DEBUG: - urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) - -handler404 = pageNotFound +""" +URL configuration for OneCprogsite project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/4.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.conf.urls.static import static +from django.contrib import admin +from django.urls import path + +from OneCprogsite import settings +from programmer.views import * +from django.urls import path, include + +urlpatterns = [ + path('admin/', admin.site.urls), + path('', include('programmer.urls')), + # path('', index, name='home'), + # path('about/', about, name='about'), + # path('solution/', solution, name='solution'), + # path('ability/', ability, name='ability'), + # path('recall/', recall, name='recall'), + # path('post/', show_post, name='post'), +] + +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + +handler404 = pageNotFound diff --git a/OneCprogsite/OneCprogsite/wsgi.py b/OneCprogsite/OneCprogsite/wsgi.py index 052d462..05e8dc4 100644 --- a/OneCprogsite/OneCprogsite/wsgi.py +++ b/OneCprogsite/OneCprogsite/wsgi.py @@ -1,16 +1,16 @@ -""" -WSGI config for OneCprogsite project. - -It exposes the WSGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ -""" - -import os - -from django.core.wsgi import get_wsgi_application - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'OneCprogsite.settings') - -application = get_wsgi_application() +""" +WSGI config for OneCprogsite project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'OneCprogsite.settings') + +application = get_wsgi_application() diff --git a/OneCprogsite/__pycache__/__init__.cpython-311.pyc b/OneCprogsite/__pycache__/__init__.cpython-311.pyc index 58696c6..1c03efb 100644 Binary files a/OneCprogsite/__pycache__/__init__.cpython-311.pyc and b/OneCprogsite/__pycache__/__init__.cpython-311.pyc differ diff --git a/OneCprogsite/__pycache__/settings.cpython-311.pyc b/OneCprogsite/__pycache__/settings.cpython-311.pyc index 2fcee3c..c6ad856 100644 Binary files a/OneCprogsite/__pycache__/settings.cpython-311.pyc and b/OneCprogsite/__pycache__/settings.cpython-311.pyc differ diff --git a/OneCprogsite/__pycache__/urls.cpython-311.pyc b/OneCprogsite/__pycache__/urls.cpython-311.pyc index 1e0f9ee..6357f1f 100644 Binary files a/OneCprogsite/__pycache__/urls.cpython-311.pyc and b/OneCprogsite/__pycache__/urls.cpython-311.pyc differ diff --git a/OneCprogsite/__pycache__/wsgi.cpython-311.pyc b/OneCprogsite/__pycache__/wsgi.cpython-311.pyc index 00d5ce1..154a827 100644 Binary files a/OneCprogsite/__pycache__/wsgi.cpython-311.pyc and b/OneCprogsite/__pycache__/wsgi.cpython-311.pyc differ diff --git a/OneCprogsite/asgi.py b/OneCprogsite/asgi.py index 928b9fd..d008858 100644 --- a/OneCprogsite/asgi.py +++ b/OneCprogsite/asgi.py @@ -1,16 +1,16 @@ -""" -ASGI config for OneCprogsite project. - -It exposes the ASGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ -""" - -import os - -from django.core.asgi import get_asgi_application - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'OneCprogsite.settings') - -application = get_asgi_application() +""" +ASGI config for OneCprogsite project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'OneCprogsite.settings') + +application = get_asgi_application() diff --git a/OneCprogsite/manage.py b/OneCprogsite/manage.py index 7fedb13..e263efa 100644 --- a/OneCprogsite/manage.py +++ b/OneCprogsite/manage.py @@ -1,22 +1,22 @@ -#!/usr/bin/env python -"""Django's command-line utility for administrative tasks.""" -import os -import sys - - -def main(): - """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'OneCprogsite.settings') - try: - from django.core.management import execute_from_command_line - except ImportError as exc: - raise ImportError( - "Couldn't import Django. Are you sure it's installed and " - "available on your PYTHONPATH environment variable? Did you " - "forget to activate a virtual environment?" - ) from exc - execute_from_command_line(sys.argv) - - -if __name__ == '__main__': - main() +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'OneCprogsite.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/OneCprogsite/programmer/admin.py b/OneCprogsite/programmer/admin.py index 5962719..b14ce10 100644 --- a/OneCprogsite/programmer/admin.py +++ b/OneCprogsite/programmer/admin.py @@ -1,169 +1,169 @@ -from django.contrib import admin -from django.utils import timezone -from django.utils.html import format_html -from django.urls import path -from django.shortcuts import render -from django.http import HttpResponseRedirect -from django.contrib import messages -from .models import * - - -class ProgrammerAdmin(admin.ModelAdmin): - list_display = ('id', 'title', 'time_create', 'photo', 'is_published') - list_display_links = ('id', 'title') - search_fields = ('title', 'content') - list_editable = ('is_published',) - list_filter = ('time_create', 'is_published') - - -class RecallAdmin(admin.ModelAdmin): - list_display = ('id', 'title', 'time_create', 'scan', 'is_published') - list_display_links = ('id', 'title') - search_fields = ('title', 'content') - list_editable = ('is_published',) - list_filter = ('time_create', 'is_published') - - -class SolutionAdmin(admin.ModelAdmin): - list_display = ('id', 'title', 'time_create', 'is_published') - list_display_links = ('id', 'title') - search_fields = ('title', 'description', 'implementation') - list_editable = ('is_published',) - list_filter = ('time_create', 'is_published') - - -class HomeAdmin(admin.ModelAdmin): - list_display = ('id', 'title', 'time_create', 'is_published') - list_display_links = ('id', 'title') - search_fields = ('title', 'content') - list_editable = ('is_published',) - list_filter = ('time_create', 'is_published') - - -@admin.register(CallbackRequest) -class CallbackAdmin(admin.ModelAdmin): - list_display = ('name', 'phone', 'email', 'time_create', 'is_processed', 'is_read', 'new_badge') - list_display_links = ('name', 'phone') - list_editable = ('is_processed', 'is_read') - list_filter = ('time_create', 'is_processed', 'is_read') - search_fields = ('name', 'phone', 'email') - readonly_fields = ('time_create',) - actions = ['mark_as_read', 'mark_as_unread', 'mark_as_processed'] - actions = ['mark_as_read', 'mark_as_unread', 'mark_as_processed', 'resend_notification'] - - def resend_notification(self, request, queryset): - from .utils.email_notifications import send_callback_notification - count = 0 - for callback in queryset: - success = send_callback_notification(callback) - if success: - count += 1 - self.message_user(request, f'Уведомления отправлены для {count} заявок') - - resend_notification.short_description = "Переотправить email уведомления" - - def new_badge(self, obj): - if not obj.is_read: - return format_html('🆕 НОВАЯ') - return "" - - new_badge.short_description = 'Статус' - - def get_queryset(self, request): - # Показываем количество непрочитанных в заголовке - unread_count = CallbackRequest.objects.filter(is_read=False).count() - if unread_count > 0: - self.message_user( - request, - f'У вас {unread_count} непрочитанных заявок!', - messages.WARNING - ) - return super().get_queryset(request) - - def mark_as_read(self, request, queryset): - updated = queryset.update(is_read=True) - self.message_user(request, f'{updated} заявок отмечены как прочитанные') - - mark_as_read.short_description = "Отметить как прочитанные" - - def mark_as_unread(self, request, queryset): - updated = queryset.update(is_read=False) - self.message_user(request, f'{updated} заявок отмечены как непрочитанные') - - mark_as_unread.short_description = "Отметить как непрочитанные" - - def mark_as_processed(self, request, queryset): - updated = queryset.update(is_processed=True) - self.message_user(request, f'{updated} заявок отмечены как обработанные') - - mark_as_processed.short_description = "Отметить как обработанные" - - # Добавляем кастомное представление для статистики - def get_urls(self): - urls = super().get_urls() - custom_urls = [ - path('callback-stats/', self.admin_site.admin_view(self.callback_stats), name='callback_stats'), - ] - return custom_urls + urls - - def callback_stats(self, request): - today = timezone.now().date() - week_ago = today - timezone.timedelta(days=7) - - stats = { - 'total': CallbackRequest.objects.count(), - 'today': CallbackRequest.objects.filter(time_create__date=today).count(), - 'week': CallbackRequest.objects.filter(time_create__date__gte=week_ago).count(), - 'unread': CallbackRequest.objects.filter(is_read=False).count(), - 'unprocessed': CallbackRequest.objects.filter(is_processed=False).count(), - } - - context = { - **self.admin_site.each_context(request), - 'title': 'Статистика заявок', - 'stats': stats, - } - return render(request, 'admin/callback_stats.html', context) - -class ProgrammerAdmin(admin.ModelAdmin): - list_display = ('id', 'title', 'time_create', 'photo', 'is_published') - list_display_links = ('id', 'title') - search_fields = ('title', 'content') - list_editable = ('is_published',) - list_filter = ('time_create', 'is_published') - -class RecallAdmin(admin.ModelAdmin): - list_display = ('id', 'title', 'time_create', 'scan', 'is_published') - list_display_links = ('id', 'title') - search_fields = ('title', 'content') - list_editable = ('is_published',) - list_filter = ('time_create', 'is_published') - -class SolutionAdmin(admin.ModelAdmin): - list_display = ('id', 'title', 'time_create', 'is_published') - list_display_links = ('id', 'title') - search_fields = ('title', 'description', 'implementation') - list_editable = ('is_published',) - list_filter = ('time_create', 'is_published') - -class HomeAdmin(admin.ModelAdmin): - list_display = ('id', 'title', 'time_create', 'is_published') - list_display_links = ('id', 'title') - search_fields = ('title', 'content') - list_editable = ('is_published',) - list_filter = ('time_create', 'is_published') - -@admin.register(PageView) -class PageViewAdmin(admin.ModelAdmin): - list_display = ['url', 'timestamp', 'ip_address'] - list_filter = ['timestamp', 'url'] - search_fields = ['url', 'ip_address'] - date_hierarchy = 'timestamp' - - def get_queryset(self, request): - return super().get_queryset(request).order_by('-timestamp') - -admin.site.register(Competence, ProgrammerAdmin) -admin.site.register(Recall, RecallAdmin) -admin.site.register(Solution, SolutionAdmin) -admin.site.register(Home, HomeAdmin) +from django.contrib import admin +from django.utils import timezone +from django.utils.html import format_html +from django.urls import path +from django.shortcuts import render +from django.http import HttpResponseRedirect +from django.contrib import messages +from .models import * + + +class ProgrammerAdmin(admin.ModelAdmin): + list_display = ('id', 'title', 'time_create', 'photo', 'is_published') + list_display_links = ('id', 'title') + search_fields = ('title', 'content') + list_editable = ('is_published',) + list_filter = ('time_create', 'is_published') + + +class RecallAdmin(admin.ModelAdmin): + list_display = ('id', 'title', 'time_create', 'scan', 'is_published') + list_display_links = ('id', 'title') + search_fields = ('title', 'content') + list_editable = ('is_published',) + list_filter = ('time_create', 'is_published') + + +class SolutionAdmin(admin.ModelAdmin): + list_display = ('id', 'title', 'time_create', 'is_published') + list_display_links = ('id', 'title') + search_fields = ('title', 'description', 'implementation') + list_editable = ('is_published',) + list_filter = ('time_create', 'is_published') + + +class HomeAdmin(admin.ModelAdmin): + list_display = ('id', 'title', 'time_create', 'is_published') + list_display_links = ('id', 'title') + search_fields = ('title', 'content') + list_editable = ('is_published',) + list_filter = ('time_create', 'is_published') + + +@admin.register(CallbackRequest) +class CallbackAdmin(admin.ModelAdmin): + list_display = ('name', 'phone', 'email', 'time_create', 'is_processed', 'is_read', 'new_badge') + list_display_links = ('name', 'phone') + list_editable = ('is_processed', 'is_read') + list_filter = ('time_create', 'is_processed', 'is_read') + search_fields = ('name', 'phone', 'email') + readonly_fields = ('time_create',) + actions = ['mark_as_read', 'mark_as_unread', 'mark_as_processed'] + actions = ['mark_as_read', 'mark_as_unread', 'mark_as_processed', 'resend_notification'] + + def resend_notification(self, request, queryset): + from .utils.email_notifications import send_callback_notification + count = 0 + for callback in queryset: + success = send_callback_notification(callback) + if success: + count += 1 + self.message_user(request, f'Уведомления отправлены для {count} заявок') + + resend_notification.short_description = "Переотправить email уведомления" + + def new_badge(self, obj): + if not obj.is_read: + return format_html('🆕 НОВАЯ') + return "" + + new_badge.short_description = 'Статус' + + def get_queryset(self, request): + # Показываем количество непрочитанных в заголовке + unread_count = CallbackRequest.objects.filter(is_read=False).count() + if unread_count > 0: + self.message_user( + request, + f'У вас {unread_count} непрочитанных заявок!', + messages.WARNING + ) + return super().get_queryset(request) + + def mark_as_read(self, request, queryset): + updated = queryset.update(is_read=True) + self.message_user(request, f'{updated} заявок отмечены как прочитанные') + + mark_as_read.short_description = "Отметить как прочитанные" + + def mark_as_unread(self, request, queryset): + updated = queryset.update(is_read=False) + self.message_user(request, f'{updated} заявок отмечены как непрочитанные') + + mark_as_unread.short_description = "Отметить как непрочитанные" + + def mark_as_processed(self, request, queryset): + updated = queryset.update(is_processed=True) + self.message_user(request, f'{updated} заявок отмечены как обработанные') + + mark_as_processed.short_description = "Отметить как обработанные" + + # Добавляем кастомное представление для статистики + def get_urls(self): + urls = super().get_urls() + custom_urls = [ + path('callback-stats/', self.admin_site.admin_view(self.callback_stats), name='callback_stats'), + ] + return custom_urls + urls + + def callback_stats(self, request): + today = timezone.now().date() + week_ago = today - timezone.timedelta(days=7) + + stats = { + 'total': CallbackRequest.objects.count(), + 'today': CallbackRequest.objects.filter(time_create__date=today).count(), + 'week': CallbackRequest.objects.filter(time_create__date__gte=week_ago).count(), + 'unread': CallbackRequest.objects.filter(is_read=False).count(), + 'unprocessed': CallbackRequest.objects.filter(is_processed=False).count(), + } + + context = { + **self.admin_site.each_context(request), + 'title': 'Статистика заявок', + 'stats': stats, + } + return render(request, 'admin/callback_stats.html', context) + +class ProgrammerAdmin(admin.ModelAdmin): + list_display = ('id', 'title', 'time_create', 'photo', 'is_published') + list_display_links = ('id', 'title') + search_fields = ('title', 'content') + list_editable = ('is_published',) + list_filter = ('time_create', 'is_published') + +class RecallAdmin(admin.ModelAdmin): + list_display = ('id', 'title', 'time_create', 'scan', 'is_published') + list_display_links = ('id', 'title') + search_fields = ('title', 'content') + list_editable = ('is_published',) + list_filter = ('time_create', 'is_published') + +class SolutionAdmin(admin.ModelAdmin): + list_display = ('id', 'title', 'time_create', 'is_published') + list_display_links = ('id', 'title') + search_fields = ('title', 'description', 'implementation') + list_editable = ('is_published',) + list_filter = ('time_create', 'is_published') + +class HomeAdmin(admin.ModelAdmin): + list_display = ('id', 'title', 'time_create', 'is_published') + list_display_links = ('id', 'title') + search_fields = ('title', 'content') + list_editable = ('is_published',) + list_filter = ('time_create', 'is_published') + +@admin.register(PageView) +class PageViewAdmin(admin.ModelAdmin): + list_display = ['url', 'timestamp', 'ip_address'] + list_filter = ['timestamp', 'url'] + search_fields = ['url', 'ip_address'] + date_hierarchy = 'timestamp' + + def get_queryset(self, request): + return super().get_queryset(request).order_by('-timestamp') + +admin.site.register(Competence, ProgrammerAdmin) +admin.site.register(Recall, RecallAdmin) +admin.site.register(Solution, SolutionAdmin) +admin.site.register(Home, HomeAdmin) diff --git a/OneCprogsite/programmer/apps.py b/OneCprogsite/programmer/apps.py index c8b6481..d96696b 100644 --- a/OneCprogsite/programmer/apps.py +++ b/OneCprogsite/programmer/apps.py @@ -1,7 +1,7 @@ -from django.apps import AppConfig - - -class ProgrammerConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'programmer' - verbose_name = 'Программисты' +from django.apps import AppConfig + + +class ProgrammerConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'programmer' + verbose_name = 'Программисты' diff --git a/OneCprogsite/programmer/context_processors.py b/OneCprogsite/programmer/context_processors.py index 41a7d3c..998554b 100644 --- a/OneCprogsite/programmer/context_processors.py +++ b/OneCprogsite/programmer/context_processors.py @@ -1,12 +1,12 @@ -from .views import menu -from django.conf import settings - - -def menu_processor(request): - return {'menu': menu} - -def contact_info(request): - return { - 'CONTACT_EMAIL': getattr(settings, 'CONTACT_EMAIL', 'it@nserdyuk.ru'), - 'CONTACT_PHONE': getattr(settings, 'CONTACT_PHONE', '+7 (960) 469-40-88'), +from .views import menu +from django.conf import settings + + +def menu_processor(request): + return {'menu': menu} + +def contact_info(request): + return { + 'CONTACT_EMAIL': getattr(settings, 'CONTACT_EMAIL', 'it@nserdyuk.ru'), + 'CONTACT_PHONE': getattr(settings, 'CONTACT_PHONE', '+7 (960) 469-40-88'), } \ No newline at end of file diff --git a/OneCprogsite/programmer/forms.py b/OneCprogsite/programmer/forms.py index 9726b2c..826ef05 100644 --- a/OneCprogsite/programmer/forms.py +++ b/OneCprogsite/programmer/forms.py @@ -1,33 +1,33 @@ -from django import forms -from .models import CallbackRequest - - -class CallbackForm(forms.ModelForm): - class Meta: - model = CallbackRequest - fields = ['name', 'phone', 'email', 'question'] - widgets = { - 'name': forms.TextInput(attrs={ - 'class': 'form-input', - 'placeholder': 'Ваше имя' - }), - 'phone': forms.TextInput(attrs={ - 'class': 'form-input', - 'placeholder': '+7 (___) ___-__-__' - }), - 'email': forms.EmailInput(attrs={ - 'class': 'form-input', - 'placeholder': 'your@email.com' - }), - 'question': forms.Textarea(attrs={ - 'class': 'form-textarea', - 'placeholder': 'Опишите ваш вопрос или задачу...', - 'rows': 4 - }), - } - labels = { - 'name': 'Имя', - 'phone': 'Телефон', - 'email': 'Электронная почта', - 'question': 'Ваш вопрос' +from django import forms +from .models import CallbackRequest + + +class CallbackForm(forms.ModelForm): + class Meta: + model = CallbackRequest + fields = ['name', 'phone', 'email', 'question'] + widgets = { + 'name': forms.TextInput(attrs={ + 'class': 'form-input', + 'placeholder': 'Ваше имя' + }), + 'phone': forms.TextInput(attrs={ + 'class': 'form-input', + 'placeholder': '+7 (___) ___-__-__' + }), + 'email': forms.EmailInput(attrs={ + 'class': 'form-input', + 'placeholder': 'your@email.com' + }), + 'question': forms.Textarea(attrs={ + 'class': 'form-textarea', + 'placeholder': 'Опишите ваш вопрос или задачу...', + 'rows': 4 + }), + } + labels = { + 'name': 'Имя', + 'phone': 'Телефон', + 'email': 'Электронная почта', + 'question': 'Ваш вопрос' } \ No newline at end of file diff --git a/OneCprogsite/programmer/management/commands/send_daily_summary.py b/OneCprogsite/programmer/management/commands/send_daily_summary.py index fadba37..cff2dcb 100644 --- a/OneCprogsite/programmer/management/commands/send_daily_summary.py +++ b/OneCprogsite/programmer/management/commands/send_daily_summary.py @@ -1,33 +1,33 @@ -# programmer/management/commands/send_daily_summary.py -from django.core.management.base import BaseCommand -from django.utils import timezone -from programmer.utils.email_notifications import send_daily_summary - - -class Command(BaseCommand): - help = 'Отправляет ежедневную сводку по заявкам на email' - - def add_arguments(self, parser): - parser.add_argument( - '--test', - action='store_true', - help='Тестовая отправка (не учитывает реальные данные)', - ) - - def handle(self, *args, **options): - if options['test']: - self.stdout.write(self.style.WARNING('Тестовая отправка ежедневной сводки...')) - # Здесь можно добавить тестовые данные - else: - self.stdout.write('Отправка ежедневной сводки...') - - success = send_daily_summary() - - if success: - self.stdout.write( - self.style.SUCCESS(f'Ежедневная сводка отправлена успешно! Время: {timezone.now()}') - ) - else: - self.stdout.write( - self.style.WARNING('Ежедневная сводка не отправлена (нет данных или ошибка)') +# programmer/management/commands/send_daily_summary.py +from django.core.management.base import BaseCommand +from django.utils import timezone +from programmer.utils.email_notifications import send_daily_summary + + +class Command(BaseCommand): + help = 'Отправляет ежедневную сводку по заявкам на email' + + def add_arguments(self, parser): + parser.add_argument( + '--test', + action='store_true', + help='Тестовая отправка (не учитывает реальные данные)', + ) + + def handle(self, *args, **options): + if options['test']: + self.stdout.write(self.style.WARNING('Тестовая отправка ежедневной сводки...')) + # Здесь можно добавить тестовые данные + else: + self.stdout.write('Отправка ежедневной сводки...') + + success = send_daily_summary() + + if success: + self.stdout.write( + self.style.SUCCESS(f'Ежедневная сводка отправлена успешно! Время: {timezone.now()}') + ) + else: + self.stdout.write( + self.style.WARNING('Ежедневная сводка не отправлена (нет данных или ошибка)') ) \ No newline at end of file diff --git a/OneCprogsite/programmer/management/commands/test_email.py b/OneCprogsite/programmer/management/commands/test_email.py index b2ccccf..bfbd10e 100644 --- a/OneCprogsite/programmer/management/commands/test_email.py +++ b/OneCprogsite/programmer/management/commands/test_email.py @@ -1,29 +1,29 @@ -# programmer/management/commands/test_email.py -from django.core.management.base import BaseCommand -from django.conf import settings -from programmer.utils.email_notifications import send_test_email - - -class Command(BaseCommand): - help = 'Test email configuration' - - def handle(self, *args, **options): - self.stdout.write("Testing email configuration...") - - # Проверяем настройки - self.stdout.write(f"EMAIL_HOST: {settings.EMAIL_HOST}") - self.stdout.write(f"EMAIL_PORT: {settings.EMAIL_PORT}") - self.stdout.write(f"EMAIL_HOST_USER: {settings.EMAIL_HOST_USER}") - self.stdout.write(f"ADMIN_EMAILS: {settings.ADMIN_EMAILS}") - - # Тестируем отправку - success = send_test_email() - - if success: - self.stdout.write( - self.style.SUCCESS('✅ Test email sent successfully!') - ) - else: - self.stdout.write( - self.style.ERROR('❌ Failed to send test email. Check your email settings.') +# programmer/management/commands/test_email.py +from django.core.management.base import BaseCommand +from django.conf import settings +from programmer.utils.email_notifications import send_test_email + + +class Command(BaseCommand): + help = 'Test email configuration' + + def handle(self, *args, **options): + self.stdout.write("Testing email configuration...") + + # Проверяем настройки + self.stdout.write(f"EMAIL_HOST: {settings.EMAIL_HOST}") + self.stdout.write(f"EMAIL_PORT: {settings.EMAIL_PORT}") + self.stdout.write(f"EMAIL_HOST_USER: {settings.EMAIL_HOST_USER}") + self.stdout.write(f"ADMIN_EMAILS: {settings.ADMIN_EMAILS}") + + # Тестируем отправку + success = send_test_email() + + if success: + self.stdout.write( + self.style.SUCCESS('✅ Test email sent successfully!') + ) + else: + self.stdout.write( + self.style.ERROR('❌ Failed to send test email. Check your email settings.') ) \ No newline at end of file diff --git a/OneCprogsite/programmer/management/commands/test_sitemap.py b/OneCprogsite/programmer/management/commands/test_sitemap.py index 338d65d..6ae9555 100644 --- a/OneCprogsite/programmer/management/commands/test_sitemap.py +++ b/OneCprogsite/programmer/management/commands/test_sitemap.py @@ -1,27 +1,27 @@ -from django.core.management.base import BaseCommand -from django.contrib.sitemaps import Sitemap -from django.urls import reverse -from programmer.models import Home, Solution, Competence, Recall - - -class Command(BaseCommand): - help = 'Test sitemap generation' - - def handle(self, *args, **options): - from programmer.sitemaps import sitemaps - - self.stdout.write('Testing sitemap generation...') - - for name, sitemap in sitemaps.items(): - self.stdout.write(f'\n{name}:') - items = sitemap().items() - self.stdout.write(f' Items found: {len(items)}') - - for item in items[:3]: # Показываем первые 3 элемента - try: - url = sitemap().location(item) - self.stdout.write(f' - {url}') - except Exception as e: - self.stdout.write(f' - Error: {e}') - - self.stdout.write('\nSitemap test completed!') +from django.core.management.base import BaseCommand +from django.contrib.sitemaps import Sitemap +from django.urls import reverse +from programmer.models import Home, Solution, Competence, Recall + + +class Command(BaseCommand): + help = 'Test sitemap generation' + + def handle(self, *args, **options): + from programmer.sitemaps import sitemaps + + self.stdout.write('Testing sitemap generation...') + + for name, sitemap in sitemaps.items(): + self.stdout.write(f'\n{name}:') + items = sitemap().items() + self.stdout.write(f' Items found: {len(items)}') + + for item in items[:3]: # Показываем первые 3 элемента + try: + url = sitemap().location(item) + self.stdout.write(f' - {url}') + except Exception as e: + self.stdout.write(f' - Error: {e}') + + self.stdout.write('\nSitemap test completed!') diff --git a/OneCprogsite/programmer/middleware.py b/OneCprogsite/programmer/middleware.py index 494461e..88ec46d 100644 --- a/OneCprogsite/programmer/middleware.py +++ b/OneCprogsite/programmer/middleware.py @@ -1,54 +1,54 @@ -from .models import PageView, Visitor -from django.utils import timezone -from django.db import transaction - - -class PageViewMiddleware: - def __init__(self, get_response): - self.get_response = get_response - - def __call__(self, request): - # Игнорируем статические файлы и админку - if not request.path.startswith('/static/') and not request.path.startswith('/admin/'): - self.track_page_view(request) - - response = self.get_response(request) - return response - - def track_page_view(self, request): - try: - with transaction.atomic(): - # Сохраняем просмотр страницы - PageView.objects.create( - url=request.path, - ip_address=self.get_client_ip(request), - user_agent=request.META.get('HTTP_USER_AGENT', ''), - referer=request.META.get('HTTP_REFERER', '') - ) - - # Обновляем статистику посетителя - ip = self.get_client_ip(request) - visitor, created = Visitor.objects.get_or_create( - ip_address=ip, - defaults={ - 'first_visit': timezone.now(), - 'last_visit': timezone.now() - } - ) - - if not created: - visitor.last_visit = timezone.now() - visitor.visit_count += 1 - visitor.save() - - except Exception as e: - # Логируем ошибку, но не прерываем выполнение - print(f"Error tracking page view: {e}") - - def get_client_ip(self, request): - x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') - if x_forwarded_for: - ip = x_forwarded_for.split(',')[0] - else: - ip = request.META.get('REMOTE_ADDR') +from .models import PageView, Visitor +from django.utils import timezone +from django.db import transaction + + +class PageViewMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + # Игнорируем статические файлы и админку + if not request.path.startswith('/static/') and not request.path.startswith('/admin/'): + self.track_page_view(request) + + response = self.get_response(request) + return response + + def track_page_view(self, request): + try: + with transaction.atomic(): + # Сохраняем просмотр страницы + PageView.objects.create( + url=request.path, + ip_address=self.get_client_ip(request), + user_agent=request.META.get('HTTP_USER_AGENT', ''), + referer=request.META.get('HTTP_REFERER', '') + ) + + # Обновляем статистику посетителя + ip = self.get_client_ip(request) + visitor, created = Visitor.objects.get_or_create( + ip_address=ip, + defaults={ + 'first_visit': timezone.now(), + 'last_visit': timezone.now() + } + ) + + if not created: + visitor.last_visit = timezone.now() + visitor.visit_count += 1 + visitor.save() + + except Exception as e: + # Логируем ошибку, но не прерываем выполнение + print(f"Error tracking page view: {e}") + + def get_client_ip(self, request): + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + if x_forwarded_for: + ip = x_forwarded_for.split(',')[0] + else: + ip = request.META.get('REMOTE_ADDR') return ip \ No newline at end of file diff --git a/OneCprogsite/programmer/migrations/0001_initial.py b/OneCprogsite/programmer/migrations/0001_initial.py index 93ba5bf..73e2bf5 100644 --- a/OneCprogsite/programmer/migrations/0001_initial.py +++ b/OneCprogsite/programmer/migrations/0001_initial.py @@ -1,26 +1,26 @@ -# Generated by Django 4.2.7 on 2023-11-23 12:47 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='Competence', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=255)), - ('content', models.TextField(blank=True)), - ('photo', models.ImageField(upload_to='photos/%Y/%m/%d/')), - ('time_create', models.DateTimeField(auto_now_add=True)), - ('time_update', models.DateTimeField(auto_now=True)), - ('is_publiched', models.BooleanField(default=True)), - ], - ), - ] +# Generated by Django 4.2.7 on 2023-11-23 12:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Competence', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255)), + ('content', models.TextField(blank=True)), + ('photo', models.ImageField(upload_to='photos/%Y/%m/%d/')), + ('time_create', models.DateTimeField(auto_now_add=True)), + ('time_update', models.DateTimeField(auto_now=True)), + ('is_publiched', models.BooleanField(default=True)), + ], + ), + ] diff --git a/OneCprogsite/programmer/migrations/0002_alter_competence_options_alter_competence_content_and_more.py b/OneCprogsite/programmer/migrations/0002_alter_competence_options_alter_competence_content_and_more.py index c1d337d..af54c6c 100644 --- a/OneCprogsite/programmer/migrations/0002_alter_competence_options_alter_competence_content_and_more.py +++ b/OneCprogsite/programmer/migrations/0002_alter_competence_options_alter_competence_content_and_more.py @@ -1,47 +1,47 @@ -# Generated by Django 4.2.7 on 2023-11-24 08:03 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('programmer', '0001_initial'), - ] - - operations = [ - migrations.AlterModelOptions( - name='competence', - options={'ordering': ['time_create', 'title'], 'verbose_name': 'Компитенция', 'verbose_name_plural': 'Компитенции'}, - ), - migrations.AlterField( - model_name='competence', - name='content', - field=models.TextField(blank=True, verbose_name='Компетенция'), - ), - migrations.AlterField( - model_name='competence', - name='is_publiched', - field=models.BooleanField(default=True, verbose_name='Опубликован'), - ), - migrations.AlterField( - model_name='competence', - name='photo', - field=models.ImageField(upload_to='photos/%Y/%m/%d/', verbose_name='Фото'), - ), - migrations.AlterField( - model_name='competence', - name='time_create', - field=models.DateTimeField(auto_now_add=True, verbose_name='Дата создания'), - ), - migrations.AlterField( - model_name='competence', - name='time_update', - field=models.DateTimeField(auto_now=True, verbose_name='Дата изменения'), - ), - migrations.AlterField( - model_name='competence', - name='title', - field=models.CharField(max_length=255, verbose_name='Программист'), - ), - ] +# Generated by Django 4.2.7 on 2023-11-24 08:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('programmer', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='competence', + options={'ordering': ['time_create', 'title'], 'verbose_name': 'Компитенция', 'verbose_name_plural': 'Компитенции'}, + ), + migrations.AlterField( + model_name='competence', + name='content', + field=models.TextField(blank=True, verbose_name='Компетенция'), + ), + migrations.AlterField( + model_name='competence', + name='is_publiched', + field=models.BooleanField(default=True, verbose_name='Опубликован'), + ), + migrations.AlterField( + model_name='competence', + name='photo', + field=models.ImageField(upload_to='photos/%Y/%m/%d/', verbose_name='Фото'), + ), + migrations.AlterField( + model_name='competence', + name='time_create', + field=models.DateTimeField(auto_now_add=True, verbose_name='Дата создания'), + ), + migrations.AlterField( + model_name='competence', + name='time_update', + field=models.DateTimeField(auto_now=True, verbose_name='Дата изменения'), + ), + migrations.AlterField( + model_name='competence', + name='title', + field=models.CharField(max_length=255, verbose_name='Программист'), + ), + ] diff --git a/OneCprogsite/programmer/migrations/0003_recall_rename_is_publiched_competence_is_published.py b/OneCprogsite/programmer/migrations/0003_recall_rename_is_publiched_competence_is_published.py index ac845b4..5943c25 100644 --- a/OneCprogsite/programmer/migrations/0003_recall_rename_is_publiched_competence_is_published.py +++ b/OneCprogsite/programmer/migrations/0003_recall_rename_is_publiched_competence_is_published.py @@ -1,35 +1,35 @@ -# Generated by Django 4.2.7 on 2023-11-24 11:54 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('programmer', '0002_alter_competence_options_alter_competence_content_and_more'), - ] - - operations = [ - migrations.CreateModel( - name='Recall', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=255, verbose_name='Организация')), - ('content', models.TextField(blank=True, verbose_name='Отзыв')), - ('photo', models.ImageField(upload_to='photos/%Y/%m/%d/', verbose_name='Фото')), - ('time_create', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')), - ('time_update', models.DateTimeField(auto_now=True, verbose_name='Дата изменения')), - ('is_published', models.BooleanField(default=True, verbose_name='Опубликован')), - ], - options={ - 'verbose_name': 'Отзыв', - 'verbose_name_plural': 'Отзывы', - 'ordering': ['time_create', 'title'], - }, - ), - migrations.RenameField( - model_name='competence', - old_name='is_publiched', - new_name='is_published', - ), - ] +# Generated by Django 4.2.7 on 2023-11-24 11:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('programmer', '0002_alter_competence_options_alter_competence_content_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='Recall', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255, verbose_name='Организация')), + ('content', models.TextField(blank=True, verbose_name='Отзыв')), + ('photo', models.ImageField(upload_to='photos/%Y/%m/%d/', verbose_name='Фото')), + ('time_create', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')), + ('time_update', models.DateTimeField(auto_now=True, verbose_name='Дата изменения')), + ('is_published', models.BooleanField(default=True, verbose_name='Опубликован')), + ], + options={ + 'verbose_name': 'Отзыв', + 'verbose_name_plural': 'Отзывы', + 'ordering': ['time_create', 'title'], + }, + ), + migrations.RenameField( + model_name='competence', + old_name='is_publiched', + new_name='is_published', + ), + ] diff --git a/OneCprogsite/programmer/migrations/0004_rename_photo_recall_scan.py b/OneCprogsite/programmer/migrations/0004_rename_photo_recall_scan.py index d366259..9bb8335 100644 --- a/OneCprogsite/programmer/migrations/0004_rename_photo_recall_scan.py +++ b/OneCprogsite/programmer/migrations/0004_rename_photo_recall_scan.py @@ -1,18 +1,18 @@ -# Generated by Django 4.2.7 on 2023-11-24 12:02 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('programmer', '0003_recall_rename_is_publiched_competence_is_published'), - ] - - operations = [ - migrations.RenameField( - model_name='recall', - old_name='photo', - new_name='scan', - ), - ] +# Generated by Django 4.2.7 on 2023-11-24 12:02 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('programmer', '0003_recall_rename_is_publiched_competence_is_published'), + ] + + operations = [ + migrations.RenameField( + model_name='recall', + old_name='photo', + new_name='scan', + ), + ] diff --git a/OneCprogsite/programmer/migrations/0005_auto_20231124_1519.py b/OneCprogsite/programmer/migrations/0005_auto_20231124_1519.py index 1daa59b..5567067 100644 --- a/OneCprogsite/programmer/migrations/0005_auto_20231124_1519.py +++ b/OneCprogsite/programmer/migrations/0005_auto_20231124_1519.py @@ -1,31 +1,31 @@ -# Generated by Django 4.2.7 on 2023-11-24 12:19 - -from django.db import migrations, models - - - -class Migration(migrations.Migration): - - dependencies = [ - ('programmer', '0004_rename_photo_recall_scan'), - ] - - operations = [ - migrations.CreateModel( - name='Recall', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=255, verbose_name='Организация')), - ('content', models.TextField(blank=True, verbose_name='Отзыв')), - ('scan', models.ImageField(upload_to='photos/%Y/%m/%d/', verbose_name='Скан')), - ('time_create', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')), - ('time_update', models.DateTimeField(auto_now=True, verbose_name='Дата изменения')), - ('is_published', models.BooleanField(default=True, verbose_name='Опубликован')), - ], - options={ - 'verbose_name': 'Отзыв', - 'verbose_name_plural': 'Отзывы', - 'ordering': ['time_create', 'title'], - }, - ), - ] +# Generated by Django 4.2.7 on 2023-11-24 12:19 + +from django.db import migrations, models + + + +class Migration(migrations.Migration): + + dependencies = [ + ('programmer', '0004_rename_photo_recall_scan'), + ] + + operations = [ + migrations.CreateModel( + name='Recall', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255, verbose_name='Организация')), + ('content', models.TextField(blank=True, verbose_name='Отзыв')), + ('scan', models.ImageField(upload_to='photos/%Y/%m/%d/', verbose_name='Скан')), + ('time_create', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')), + ('time_update', models.DateTimeField(auto_now=True, verbose_name='Дата изменения')), + ('is_published', models.BooleanField(default=True, verbose_name='Опубликован')), + ], + options={ + 'verbose_name': 'Отзыв', + 'verbose_name_plural': 'Отзывы', + 'ordering': ['time_create', 'title'], + }, + ), + ] diff --git a/OneCprogsite/programmer/migrations/0006_alter_recall_scan.py b/OneCprogsite/programmer/migrations/0006_alter_recall_scan.py index 8809d90..403ecd1 100644 --- a/OneCprogsite/programmer/migrations/0006_alter_recall_scan.py +++ b/OneCprogsite/programmer/migrations/0006_alter_recall_scan.py @@ -1,18 +1,18 @@ -# Generated by Django 4.2.7 on 2023-11-25 09:18 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('programmer', '0005_auto_20231124_1519'), - ] - - operations = [ - migrations.AlterField( - model_name='recall', - name='scan', - field=models.ImageField(upload_to='scan/%Y/%m/%d/', verbose_name='Фото'), - ), - ] +# Generated by Django 4.2.7 on 2023-11-25 09:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('programmer', '0005_auto_20231124_1519'), + ] + + operations = [ + migrations.AlterField( + model_name='recall', + name='scan', + field=models.ImageField(upload_to='scan/%Y/%m/%d/', verbose_name='Фото'), + ), + ] diff --git a/OneCprogsite/programmer/migrations/0007_solution.py b/OneCprogsite/programmer/migrations/0007_solution.py index 9500897..8e14c40 100644 --- a/OneCprogsite/programmer/migrations/0007_solution.py +++ b/OneCprogsite/programmer/migrations/0007_solution.py @@ -1,31 +1,31 @@ -# Generated by Django 4.2.7 on 2023-11-25 09:51 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('programmer', '0006_alter_recall_scan'), - ] - - operations = [ - migrations.CreateModel( - name='Solution', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=255, verbose_name='Наименование')), - ('description', models.TextField(blank=True, verbose_name='Описание')), - ('implementation', models.TextField(blank=True, verbose_name='Реализация')), - ('closing', models.TextField(blank=True, verbose_name='Заключение')), - ('time_create', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')), - ('time_update', models.DateTimeField(auto_now=True, verbose_name='Дата изменения')), - ('is_published', models.BooleanField(default=True, verbose_name='Опубликован')), - ], - options={ - 'verbose_name': 'Проекты', - 'verbose_name_plural': 'Проекты', - 'ordering': ['time_create', 'title'], - }, - ), - ] +# Generated by Django 4.2.7 on 2023-11-25 09:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('programmer', '0006_alter_recall_scan'), + ] + + operations = [ + migrations.CreateModel( + name='Solution', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255, verbose_name='Наименование')), + ('description', models.TextField(blank=True, verbose_name='Описание')), + ('implementation', models.TextField(blank=True, verbose_name='Реализация')), + ('closing', models.TextField(blank=True, verbose_name='Заключение')), + ('time_create', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')), + ('time_update', models.DateTimeField(auto_now=True, verbose_name='Дата изменения')), + ('is_published', models.BooleanField(default=True, verbose_name='Опубликован')), + ], + options={ + 'verbose_name': 'Проекты', + 'verbose_name_plural': 'Проекты', + 'ordering': ['time_create', 'title'], + }, + ), + ] diff --git a/OneCprogsite/programmer/migrations/0008_home.py b/OneCprogsite/programmer/migrations/0008_home.py index 14e0ac8..0f8d897 100644 --- a/OneCprogsite/programmer/migrations/0008_home.py +++ b/OneCprogsite/programmer/migrations/0008_home.py @@ -1,30 +1,30 @@ -# Generated by Django 4.2.7 on 2023-11-25 10:45 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('programmer', '0007_solution'), - ] - - operations = [ - migrations.CreateModel( - name='Home', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=255, verbose_name='Наименование')), - ('content', models.TextField(blank=True, verbose_name='Статья')), - ('home_image', models.ImageField(upload_to='home_image/%Y/%m/%d/', verbose_name='Фото')), - ('time_create', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')), - ('time_update', models.DateTimeField(auto_now=True, verbose_name='Дата изменения')), - ('is_published', models.BooleanField(default=True, verbose_name='Опубликован')), - ], - options={ - 'verbose_name': 'Главная страница', - 'verbose_name_plural': 'Главная страница', - 'ordering': ['time_create', 'title'], - }, - ), - ] +# Generated by Django 4.2.7 on 2023-11-25 10:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('programmer', '0007_solution'), + ] + + operations = [ + migrations.CreateModel( + name='Home', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255, verbose_name='Наименование')), + ('content', models.TextField(blank=True, verbose_name='Статья')), + ('home_image', models.ImageField(upload_to='home_image/%Y/%m/%d/', verbose_name='Фото')), + ('time_create', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')), + ('time_update', models.DateTimeField(auto_now=True, verbose_name='Дата изменения')), + ('is_published', models.BooleanField(default=True, verbose_name='Опубликован')), + ], + options={ + 'verbose_name': 'Главная страница', + 'verbose_name_plural': 'Главная страница', + 'ordering': ['time_create', 'title'], + }, + ), + ] diff --git a/OneCprogsite/programmer/migrations/0009_callbackrequest_alter_competence_options.py b/OneCprogsite/programmer/migrations/0009_callbackrequest_alter_competence_options.py index 1b292b8..3a8582b 100644 --- a/OneCprogsite/programmer/migrations/0009_callbackrequest_alter_competence_options.py +++ b/OneCprogsite/programmer/migrations/0009_callbackrequest_alter_competence_options.py @@ -1,34 +1,34 @@ -# Generated by Django 4.2.7 on 2025-11-09 12:00 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('programmer', '0008_home'), - ] - - operations = [ - migrations.CreateModel( - name='CallbackRequest', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100, verbose_name='Имя')), - ('phone', models.CharField(max_length=20, verbose_name='Телефон')), - ('email', models.EmailField(max_length=254, verbose_name='Электронная почта')), - ('question', models.TextField(verbose_name='Ваш вопрос')), - ('time_create', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')), - ('is_processed', models.BooleanField(default=False, verbose_name='Обработано')), - ], - options={ - 'verbose_name': 'Заявка на звонок', - 'verbose_name_plural': 'Заявки на звонок', - 'ordering': ['-time_create'], - }, - ), - migrations.AlterModelOptions( - name='competence', - options={'ordering': ['time_create', 'title'], 'verbose_name': 'Компетенция', 'verbose_name_plural': 'Компетенции'}, - ), - ] +# Generated by Django 4.2.7 on 2025-11-09 12:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('programmer', '0008_home'), + ] + + operations = [ + migrations.CreateModel( + name='CallbackRequest', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, verbose_name='Имя')), + ('phone', models.CharField(max_length=20, verbose_name='Телефон')), + ('email', models.EmailField(max_length=254, verbose_name='Электронная почта')), + ('question', models.TextField(verbose_name='Ваш вопрос')), + ('time_create', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')), + ('is_processed', models.BooleanField(default=False, verbose_name='Обработано')), + ], + options={ + 'verbose_name': 'Заявка на звонок', + 'verbose_name_plural': 'Заявки на звонок', + 'ordering': ['-time_create'], + }, + ), + migrations.AlterModelOptions( + name='competence', + options={'ordering': ['time_create', 'title'], 'verbose_name': 'Компетенция', 'verbose_name_plural': 'Компетенции'}, + ), + ] diff --git a/OneCprogsite/programmer/migrations/0010_alter_callbackrequest_email_and_more.py b/OneCprogsite/programmer/migrations/0010_alter_callbackrequest_email_and_more.py index adfb8ba..fc93190 100644 --- a/OneCprogsite/programmer/migrations/0010_alter_callbackrequest_email_and_more.py +++ b/OneCprogsite/programmer/migrations/0010_alter_callbackrequest_email_and_more.py @@ -1,23 +1,23 @@ -# Generated by Django 4.2.7 on 2025-11-09 12:09 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('programmer', '0009_callbackrequest_alter_competence_options'), - ] - - operations = [ - migrations.AlterField( - model_name='callbackrequest', - name='email', - field=models.EmailField(blank=True, max_length=254, null=True, verbose_name='Электронная почта'), - ), - migrations.AlterField( - model_name='callbackrequest', - name='question', - field=models.TextField(blank=True, verbose_name='Ваш вопрос'), - ), - ] +# Generated by Django 4.2.7 on 2025-11-09 12:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('programmer', '0009_callbackrequest_alter_competence_options'), + ] + + operations = [ + migrations.AlterField( + model_name='callbackrequest', + name='email', + field=models.EmailField(blank=True, max_length=254, null=True, verbose_name='Электронная почта'), + ), + migrations.AlterField( + model_name='callbackrequest', + name='question', + field=models.TextField(blank=True, verbose_name='Ваш вопрос'), + ), + ] diff --git a/OneCprogsite/programmer/migrations/0011_visitor_pageview.py b/OneCprogsite/programmer/migrations/0011_visitor_pageview.py index cbe2d9d..ab5e555 100644 --- a/OneCprogsite/programmer/migrations/0011_visitor_pageview.py +++ b/OneCprogsite/programmer/migrations/0011_visitor_pageview.py @@ -1,41 +1,41 @@ -# Generated by Django 4.2.7 on 2025-11-12 11:43 - -from django.db import migrations, models -import django.utils.timezone - - -class Migration(migrations.Migration): - - dependencies = [ - ('programmer', '0010_alter_callbackrequest_email_and_more'), - ] - - operations = [ - migrations.CreateModel( - name='Visitor', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('ip_address', models.GenericIPAddressField()), - ('first_visit', models.DateTimeField(default=django.utils.timezone.now)), - ('last_visit', models.DateTimeField(default=django.utils.timezone.now)), - ('visit_count', models.IntegerField(default=1)), - ], - options={ - 'indexes': [models.Index(fields=['ip_address'], name='programmer__ip_addr_2c6dca_idx')], - }, - ), - migrations.CreateModel( - name='PageView', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('url', models.CharField(max_length=500)), - ('timestamp', models.DateTimeField(default=django.utils.timezone.now)), - ('ip_address', models.GenericIPAddressField()), - ('user_agent', models.TextField(blank=True)), - ('referer', models.CharField(blank=True, max_length=500)), - ], - options={ - 'indexes': [models.Index(fields=['url', 'timestamp'], name='programmer__url_9a41b2_idx'), models.Index(fields=['timestamp'], name='programmer__timesta_070072_idx')], - }, - ), - ] +# Generated by Django 4.2.7 on 2025-11-12 11:43 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('programmer', '0010_alter_callbackrequest_email_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='Visitor', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('ip_address', models.GenericIPAddressField()), + ('first_visit', models.DateTimeField(default=django.utils.timezone.now)), + ('last_visit', models.DateTimeField(default=django.utils.timezone.now)), + ('visit_count', models.IntegerField(default=1)), + ], + options={ + 'indexes': [models.Index(fields=['ip_address'], name='programmer__ip_addr_2c6dca_idx')], + }, + ), + migrations.CreateModel( + name='PageView', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('url', models.CharField(max_length=500)), + ('timestamp', models.DateTimeField(default=django.utils.timezone.now)), + ('ip_address', models.GenericIPAddressField()), + ('user_agent', models.TextField(blank=True)), + ('referer', models.CharField(blank=True, max_length=500)), + ], + options={ + 'indexes': [models.Index(fields=['url', 'timestamp'], name='programmer__url_9a41b2_idx'), models.Index(fields=['timestamp'], name='programmer__timesta_070072_idx')], + }, + ), + ] diff --git a/OneCprogsite/programmer/migrations/0012_callbackrequest_is_read.py b/OneCprogsite/programmer/migrations/0012_callbackrequest_is_read.py index 080dfee..6ddb498 100644 --- a/OneCprogsite/programmer/migrations/0012_callbackrequest_is_read.py +++ b/OneCprogsite/programmer/migrations/0012_callbackrequest_is_read.py @@ -1,18 +1,18 @@ -# Generated by Django 4.2.7 on 2025-11-14 09:37 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('programmer', '0011_visitor_pageview'), - ] - - operations = [ - migrations.AddField( - model_name='callbackrequest', - name='is_read', - field=models.BooleanField(default=False, verbose_name='Прочитано'), - ), - ] +# Generated by Django 4.2.7 on 2025-11-14 09:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('programmer', '0011_visitor_pageview'), + ] + + operations = [ + migrations.AddField( + model_name='callbackrequest', + name='is_read', + field=models.BooleanField(default=False, verbose_name='Прочитано'), + ), + ] diff --git a/OneCprogsite/programmer/migrations/0013_callbackrequest_notification_sent.py b/OneCprogsite/programmer/migrations/0013_callbackrequest_notification_sent.py index 7308578..23739b4 100644 --- a/OneCprogsite/programmer/migrations/0013_callbackrequest_notification_sent.py +++ b/OneCprogsite/programmer/migrations/0013_callbackrequest_notification_sent.py @@ -1,18 +1,18 @@ -# Generated by Django 4.2.7 on 2025-11-14 10:03 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('programmer', '0012_callbackrequest_is_read'), - ] - - operations = [ - migrations.AddField( - model_name='callbackrequest', - name='notification_sent', - field=models.BooleanField(default=False, verbose_name='Уведомление отправлено'), - ), - ] +# Generated by Django 4.2.7 on 2025-11-14 10:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('programmer', '0012_callbackrequest_is_read'), + ] + + operations = [ + migrations.AddField( + model_name='callbackrequest', + name='notification_sent', + field=models.BooleanField(default=False, verbose_name='Уведомление отправлено'), + ), + ] diff --git a/OneCprogsite/programmer/models.py b/OneCprogsite/programmer/models.py index b62f79b..11bc2d1 100644 --- a/OneCprogsite/programmer/models.py +++ b/OneCprogsite/programmer/models.py @@ -1,180 +1,180 @@ -from django.db import models -from django.urls import reverse -from django.utils import timezone -from django.db.models.signals import post_save, post_delete -from django.dispatch import receiver -from django.core.cache import cache -from .utils.email_notifications import send_callback_notification - - -class Recall(models.Model): - title = models.CharField(max_length=255, verbose_name='Организация') - content = models.TextField(blank=True, verbose_name='Отзыв') - scan = models.ImageField(upload_to="scan/%Y/%m/%d/", verbose_name='Фото') - time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания') - time_update = models.DateTimeField(auto_now=True, verbose_name='Дата изменения') - is_published = models.BooleanField(default=True, verbose_name='Опубликован') - - def __str__(self): - return self.title - - def get_absolute_url(self): - return reverse('post', kwargs={'post_id': self.pk}) - - class Meta: - verbose_name = 'Отзыв' - verbose_name_plural = 'Отзывы' - ordering = ['time_create', 'title'] - - def get_seo_title(self): - return f"Отзыв от {self.title} | Программист 1С" - - def get_seo_description(self): - if self.content: - clean_content = self.content[:160].replace('\n', ' ').strip() - return f"Отзыв о работе программиста 1С от {self.title}. {clean_content}..." - return f"Отзыв клиента {self.title} о работе программиста 1С Николая Сердюк" - - -class Competence(models.Model): - title = models.CharField(max_length=255, verbose_name='Программист') - content = models.TextField(blank=True, verbose_name='Компетенция') - photo = models.ImageField(upload_to="photos/%Y/%m/%d/", verbose_name='Фото') - time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания') - time_update = models.DateTimeField(auto_now=True, verbose_name='Дата изменения') - is_published = models.BooleanField(default=True, verbose_name='Опубликован') - - def __str__(self): - return self.title - - def get_absolute_url(self): - return reverse('post', kwargs={'post_id': self.pk}) - - class Meta: - verbose_name = 'Компетенция' - verbose_name_plural = 'Компетенции' - ordering = ['time_create', 'title'] - - -class Solution(models.Model): - title = models.CharField(max_length=255, verbose_name='Наименование') - description = models.TextField(blank=True, verbose_name='Описание') - implementation = models.TextField(blank=True, verbose_name='Реализация') - closing = models.TextField(blank=True, verbose_name='Заключение') - time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания') - time_update = models.DateTimeField(auto_now=True, verbose_name='Дата изменения') - is_published = models.BooleanField(default=True, verbose_name='Опубликован') - - def __str__(self): - return self.title - - def get_absolute_url(self): - return reverse('post', kwargs={'post_id': self.pk}) - - class Meta: - verbose_name = 'Проекты' - verbose_name_plural = 'Проекты' - ordering = ['time_create', 'title'] - - def get_seo_title(self): - """Генерирует SEO-заголовок для проекта""" - return f"Проект: {self.title} | Автоматизация 1С" - - def get_seo_description(self): - """Генерирует SEO-описание для проекта""" - if self.description: - clean_desc = self.description[:160].replace('\n', ' ').strip() - return f"Проект автоматизации: {self.title}. {clean_desc}..." - return f"Реализация проекта {self.title} - программист 1С Николай Сердюк" - - def get_meta_keywords(self): - """Автоматические ключевые слова для проекта""" - base_keywords = ["проект 1С", "автоматизация 1С", "внедрение 1С"] - title_words = self.title.lower().split() - return base_keywords + title_words - - -class Home(models.Model): - title = models.CharField(max_length=255, verbose_name='Наименование') - content = models.TextField(blank=True, verbose_name='Статья') - home_image = models.ImageField(upload_to="home_image/%Y/%m/%d/", verbose_name='Фото') - time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания') - time_update = models.DateTimeField(auto_now=True, verbose_name='Дата изменения') - is_published = models.BooleanField(default=True, verbose_name='Опубликован') - - def __str__(self): - return self.title - - def get_absolute_url(self): - return reverse('post', kwargs={'post_id': self.pk}) - - class Meta: - verbose_name = 'Главная страница' - verbose_name_plural = 'Главная страница' - ordering = ['time_create', 'title'] - - -class CallbackRequest(models.Model): - name = models.CharField(max_length=100, verbose_name='Имя') - phone = models.CharField(max_length=20, verbose_name='Телефон') - email = models.EmailField(blank=True, null=True, verbose_name='Электронная почта') # Сделать необязательным - question = models.TextField(blank=True, verbose_name='Ваш вопрос') # Сделать необязательным - time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания') - is_processed = models.BooleanField(default=False, verbose_name='Обработано') - is_read = models.BooleanField(default=False, verbose_name='Прочитано') - notification_sent = models.BooleanField(default=False, verbose_name='Уведомление отправлено') - - def __str__(self): - return f"{self.name} - {self.phone}" - - class Meta: - verbose_name = 'Заявка на звонок' - verbose_name_plural = 'Заявки на звонок' - ordering = ['-time_create'] - - -# Сигнал для отправки уведомления при создании заявки -@receiver(post_save, sender=CallbackRequest) -def send_callback_email_notification(sender, instance, created, **kwargs): - if created and not instance.notification_sent: - # Отправляем email уведомление - success = send_callback_notification(instance) - if success: - instance.notification_sent = True - # Сохраняем без повторного вызова сигнала - sender.objects.filter(pk=instance.pk).update(notification_sent=True) - - -class PageView(models.Model): - url = models.CharField(max_length=500) - timestamp = models.DateTimeField(default=timezone.now) - ip_address = models.GenericIPAddressField() - user_agent = models.TextField(blank=True) - referer = models.CharField(max_length=500, blank=True) - - class Meta: - indexes = [ - models.Index(fields=['url', 'timestamp']), - models.Index(fields=['timestamp']), - ] - - -class Visitor(models.Model): - ip_address = models.GenericIPAddressField() - first_visit = models.DateTimeField(default=timezone.now) - last_visit = models.DateTimeField(default=timezone.now) - visit_count = models.IntegerField(default=1) - - class Meta: - indexes = [ - models.Index(fields=['ip_address']), - ] - - -@receiver([post_save, post_delete], sender=Home) -@receiver([post_save, post_delete], sender=Solution) -@receiver([post_save, post_delete], sender=Competence) -@receiver([post_save, post_delete], sender=Recall) -def clear_sitemap_cache(sender, **kwargs): - """Очищаем кэш sitemap при изменении контента""" - cache.delete('sitemap_cache') +from django.db import models +from django.urls import reverse +from django.utils import timezone +from django.db.models.signals import post_save, post_delete +from django.dispatch import receiver +from django.core.cache import cache +from .utils.email_notifications import send_callback_notification + + +class Recall(models.Model): + title = models.CharField(max_length=255, verbose_name='Организация') + content = models.TextField(blank=True, verbose_name='Отзыв') + scan = models.ImageField(upload_to="scan/%Y/%m/%d/", verbose_name='Фото') + time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания') + time_update = models.DateTimeField(auto_now=True, verbose_name='Дата изменения') + is_published = models.BooleanField(default=True, verbose_name='Опубликован') + + def __str__(self): + return self.title + + def get_absolute_url(self): + return reverse('post', kwargs={'post_id': self.pk}) + + class Meta: + verbose_name = 'Отзыв' + verbose_name_plural = 'Отзывы' + ordering = ['time_create', 'title'] + + def get_seo_title(self): + return f"Отзыв от {self.title} | Программист 1С" + + def get_seo_description(self): + if self.content: + clean_content = self.content[:160].replace('\n', ' ').strip() + return f"Отзыв о работе программиста 1С от {self.title}. {clean_content}..." + return f"Отзыв клиента {self.title} о работе программиста 1С Николая Сердюк" + + +class Competence(models.Model): + title = models.CharField(max_length=255, verbose_name='Программист') + content = models.TextField(blank=True, verbose_name='Компетенция') + photo = models.ImageField(upload_to="photos/%Y/%m/%d/", verbose_name='Фото') + time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания') + time_update = models.DateTimeField(auto_now=True, verbose_name='Дата изменения') + is_published = models.BooleanField(default=True, verbose_name='Опубликован') + + def __str__(self): + return self.title + + def get_absolute_url(self): + return reverse('post', kwargs={'post_id': self.pk}) + + class Meta: + verbose_name = 'Компетенция' + verbose_name_plural = 'Компетенции' + ordering = ['time_create', 'title'] + + +class Solution(models.Model): + title = models.CharField(max_length=255, verbose_name='Наименование') + description = models.TextField(blank=True, verbose_name='Описание') + implementation = models.TextField(blank=True, verbose_name='Реализация') + closing = models.TextField(blank=True, verbose_name='Заключение') + time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания') + time_update = models.DateTimeField(auto_now=True, verbose_name='Дата изменения') + is_published = models.BooleanField(default=True, verbose_name='Опубликован') + + def __str__(self): + return self.title + + def get_absolute_url(self): + return reverse('post', kwargs={'post_id': self.pk}) + + class Meta: + verbose_name = 'Проекты' + verbose_name_plural = 'Проекты' + ordering = ['time_create', 'title'] + + def get_seo_title(self): + """Генерирует SEO-заголовок для проекта""" + return f"Проект: {self.title} | Автоматизация 1С" + + def get_seo_description(self): + """Генерирует SEO-описание для проекта""" + if self.description: + clean_desc = self.description[:160].replace('\n', ' ').strip() + return f"Проект автоматизации: {self.title}. {clean_desc}..." + return f"Реализация проекта {self.title} - программист 1С Николай Сердюк" + + def get_meta_keywords(self): + """Автоматические ключевые слова для проекта""" + base_keywords = ["проект 1С", "автоматизация 1С", "внедрение 1С"] + title_words = self.title.lower().split() + return base_keywords + title_words + + +class Home(models.Model): + title = models.CharField(max_length=255, verbose_name='Наименование') + content = models.TextField(blank=True, verbose_name='Статья') + home_image = models.ImageField(upload_to="home_image/%Y/%m/%d/", verbose_name='Фото') + time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания') + time_update = models.DateTimeField(auto_now=True, verbose_name='Дата изменения') + is_published = models.BooleanField(default=True, verbose_name='Опубликован') + + def __str__(self): + return self.title + + def get_absolute_url(self): + return reverse('post', kwargs={'post_id': self.pk}) + + class Meta: + verbose_name = 'Главная страница' + verbose_name_plural = 'Главная страница' + ordering = ['time_create', 'title'] + + +class CallbackRequest(models.Model): + name = models.CharField(max_length=100, verbose_name='Имя') + phone = models.CharField(max_length=20, verbose_name='Телефон') + email = models.EmailField(blank=True, null=True, verbose_name='Электронная почта') # Сделать необязательным + question = models.TextField(blank=True, verbose_name='Ваш вопрос') # Сделать необязательным + time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания') + is_processed = models.BooleanField(default=False, verbose_name='Обработано') + is_read = models.BooleanField(default=False, verbose_name='Прочитано') + notification_sent = models.BooleanField(default=False, verbose_name='Уведомление отправлено') + + def __str__(self): + return f"{self.name} - {self.phone}" + + class Meta: + verbose_name = 'Заявка на звонок' + verbose_name_plural = 'Заявки на звонок' + ordering = ['-time_create'] + + +# Сигнал для отправки уведомления при создании заявки +@receiver(post_save, sender=CallbackRequest) +def send_callback_email_notification(sender, instance, created, **kwargs): + if created and not instance.notification_sent: + # Отправляем email уведомление + success = send_callback_notification(instance) + if success: + instance.notification_sent = True + # Сохраняем без повторного вызова сигнала + sender.objects.filter(pk=instance.pk).update(notification_sent=True) + + +class PageView(models.Model): + url = models.CharField(max_length=500) + timestamp = models.DateTimeField(default=timezone.now) + ip_address = models.GenericIPAddressField() + user_agent = models.TextField(blank=True) + referer = models.CharField(max_length=500, blank=True) + + class Meta: + indexes = [ + models.Index(fields=['url', 'timestamp']), + models.Index(fields=['timestamp']), + ] + + +class Visitor(models.Model): + ip_address = models.GenericIPAddressField() + first_visit = models.DateTimeField(default=timezone.now) + last_visit = models.DateTimeField(default=timezone.now) + visit_count = models.IntegerField(default=1) + + class Meta: + indexes = [ + models.Index(fields=['ip_address']), + ] + + +@receiver([post_save, post_delete], sender=Home) +@receiver([post_save, post_delete], sender=Solution) +@receiver([post_save, post_delete], sender=Competence) +@receiver([post_save, post_delete], sender=Recall) +def clear_sitemap_cache(sender, **kwargs): + """Очищаем кэш sitemap при изменении контента""" + cache.delete('sitemap_cache') diff --git a/OneCprogsite/programmer/sitemaps.py b/OneCprogsite/programmer/sitemaps.py index e71f587..d0c1132 100644 --- a/OneCprogsite/programmer/sitemaps.py +++ b/OneCprogsite/programmer/sitemaps.py @@ -1,64 +1,64 @@ -from django.contrib.sitemaps import Sitemap -from django.urls import reverse -from .models import Home, Solution, Competence, Recall - -class StaticViewSitemap(Sitemap): - priority = 1.0 - changefreq = 'monthly' - - def items(self): - return ['home', 'about', 'solution', 'ability', 'recall'] - - def location(self, item): - return reverse(item) - -class HomeSitemap(Sitemap): - changefreq = 'weekly' - priority = 1.0 - - def items(self): - return Home.objects.filter(is_published=True) - - def lastmod(self, obj): - return obj.time_update - - # УБИРАЕМ метод location - используем default - # Django автоматически сгенерирует правильные URL - -class SolutionSitemap(Sitemap): - changefreq = 'weekly' - priority = 0.9 - - def items(self): - return Solution.objects.filter(is_published=True) - - def lastmod(self, obj): - return obj.time_update - -class CompetenceSitemap(Sitemap): - changefreq = 'monthly' - priority = 0.8 - - def items(self): - return Competence.objects.filter(is_published=True) - - def lastmod(self, obj): - return obj.time_update - -class RecallSitemap(Sitemap): - changefreq = 'monthly' - priority = 0.7 - - def items(self): - return Recall.objects.filter(is_published=True) - - def lastmod(self, obj): - return obj.time_update - -# Упрощаем sitemaps - убираем HomeSitemap если он дублирует главную -sitemaps = { - 'static': StaticViewSitemap, - 'solutions': SolutionSitemap, - 'competence': CompetenceSitemap, - 'recall': RecallSitemap, +from django.contrib.sitemaps import Sitemap +from django.urls import reverse +from .models import Home, Solution, Competence, Recall + +class StaticViewSitemap(Sitemap): + priority = 1.0 + changefreq = 'monthly' + + def items(self): + return ['home', 'about', 'solution', 'ability', 'recall'] + + def location(self, item): + return reverse(item) + +class HomeSitemap(Sitemap): + changefreq = 'weekly' + priority = 1.0 + + def items(self): + return Home.objects.filter(is_published=True) + + def lastmod(self, obj): + return obj.time_update + + # УБИРАЕМ метод location - используем default + # Django автоматически сгенерирует правильные URL + +class SolutionSitemap(Sitemap): + changefreq = 'weekly' + priority = 0.9 + + def items(self): + return Solution.objects.filter(is_published=True) + + def lastmod(self, obj): + return obj.time_update + +class CompetenceSitemap(Sitemap): + changefreq = 'monthly' + priority = 0.8 + + def items(self): + return Competence.objects.filter(is_published=True) + + def lastmod(self, obj): + return obj.time_update + +class RecallSitemap(Sitemap): + changefreq = 'monthly' + priority = 0.7 + + def items(self): + return Recall.objects.filter(is_published=True) + + def lastmod(self, obj): + return obj.time_update + +# Упрощаем sitemaps - убираем HomeSitemap если он дублирует главную +sitemaps = { + 'static': StaticViewSitemap, + 'solutions': SolutionSitemap, + 'competence': CompetenceSitemap, + 'recall': RecallSitemap, } \ No newline at end of file diff --git a/OneCprogsite/programmer/static/programmer/css/competence.css b/OneCprogsite/programmer/static/programmer/css/competence.css index c94820d..5073dab 100644 --- a/OneCprogsite/programmer/static/programmer/css/competence.css +++ b/OneCprogsite/programmer/static/programmer/css/competence.css @@ -1,299 +1,299 @@ -/* competence.css - Стили для страницы компетенций */ - -/* Основные стили для страницы компетенций */ -.competence-item { - display: flex; - gap: 2rem; - align-items: flex-start; - padding: 2rem; - background: var(--bg-card); - border-radius: var(--radius-xl); - box-shadow: var(--shadow-lg); - border-left: 4px solid var(--secondary); - transition: var(--transition); - border: 1px solid var(--border-light); -} - -.competence-item:hover { - transform: translateX(8px); - box-shadow: var(--shadow-xl); - border-color: var(--primary-light); -} - -.competence-scan-wrapper { - flex-shrink: 0; -} - -.competence-scan-container { - width: 280px; - cursor: pointer; - border-radius: var(--radius-lg); - overflow: hidden; - border: 2px solid var(--border-light); - transition: var(--transition); - position: relative; - box-shadow: var(--shadow-md); -} - -.competence-scan-container:hover { - transform: translateY(-4px) scale(1.02); - box-shadow: var(--shadow-xl); - border-color: var(--primary); -} - -.competence-scan { - width: 100%; - height: auto; - display: block; - transition: var(--transition); -} - -.competence-content { - flex: 1; - min-width: 0; -} - -.competence-title { - font-size: 1.5rem; - font-weight: 700; - color: var(--text-primary); - margin-bottom: 1rem; - background: var(--gradient-primary); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.competence-description { - line-height: 1.7; - font-size: 1.05rem; - color: var(--text-primary); -} - -.competence-description p { - margin-bottom: 1rem; -} - -.competence-description p:last-child { - margin-bottom: 0; -} - -.scan-hint { - position: absolute; - bottom: 0; - left: 0; - right: 0; - background: linear-gradient(transparent, rgba(0,0,0,0.8)); - color: white; - padding: 1rem; - text-align: center; - opacity: 0; - transition: var(--transition); - transform: translateY(10px); -} - -.competence-scan-container:hover .scan-hint { - opacity: 1; - transform: translateY(0); -} - -/* Стили для модального окна с изображением компетенций */ -.modal.competence-modal { - background-color: rgba(15, 19, 31, 0.95); - backdrop-filter: blur(10px); -} - -.modal.competence-modal .modal-content { - background: transparent; - border: none; - box-shadow: none; - max-width: 95vw; - max-height: 95vh; - margin: 2% auto; -} - -.modal.competence-modal .modal-header { - background: var(--bg-card); - border-bottom: 2px solid var(--border-light); - padding: 1.5rem 2rem; -} - -.modal.competence-modal .modal-header h3 { - margin: 0; - color: var(--text-primary); - font-size: 1.25rem; - font-weight: 600; - background: var(--gradient-primary); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.modal.competence-modal .modal-body { - padding: 1rem; - display: flex; - align-items: center; - justify-content: center; - background: transparent; -} - -.modal-image { - max-width: 90vw; - max-height: 80vh; - width: auto; - height: auto; - display: block; - margin: 0 auto; - border-radius: var(--radius-md); - box-shadow: var(--shadow-xl); - transition: all 0.3s ease; -} - -/* Анимации для модального окна */ -.modal.competence-modal { - transition: opacity 0.3s ease; - opacity: 0; -} - -.modal.competence-modal.active { - opacity: 1; -} - -.modal.competence-modal .modal-content { - transform: scale(0.7); - transition: transform 0.3s ease; -} - -.modal.competence-modal.active .modal-content { - transform: scale(1); -} - -/* Улучшенные тени и границы */ -.competence-scan-container { - border: 2px solid var(--border-light); - box-shadow: var(--shadow-lg); -} - -.competence-scan-container:hover { - border-color: var(--primary-light); - box-shadow: var(--shadow-xl); -} - -/* Адаптивность для мобильных устройств */ -@media (max-width: 768px) { - .competence-item { - flex-direction: column; - padding: 1.5rem; - gap: 1.5rem; - } - - .competence-scan-container { - width: 100%; - max-width: 300px; - margin: 0 auto; - } - - .competence-title { - font-size: 1.375rem; - text-align: center; - } - - .modal-image { - max-width: 95vw; - max-height: 70vh; - } - - .modal.competence-modal .modal-content { - margin: 10% auto; - } - - .modal.competence-modal .modal-header { - padding: 1rem 1.5rem; - } -} - -@media (max-width: 480px) { - .competence-item { - padding: 1.25rem; - } - - .competence-scan-container { - max-width: 100%; - } - - .competence-title { - font-size: 1.25rem; - } - - .modal.competence-modal .modal-content { - margin: 5% auto; - max-width: 98vw; - } - - .modal.competence-modal .modal-body { - padding: 0.5rem; - } - - .modal.competence-modal .modal-header { - padding: 1rem; - } - - .modal.competence-modal .modal-header h3 { - font-size: 1.1rem; - } -} - -/* Стили для светлой темы */ -@media (prefers-color-scheme: light) { - .modal.competence-modal { - background-color: rgba(0, 0, 0, 0.8); - } - - .modal.competence-modal .modal-header { - background: var(--bg-primary); - } - - .scan-hint { - background: linear-gradient(transparent, rgba(0,0,0,0.7)); - } - - .competence-item { - background: var(--bg-primary); - } - - .competence-description { - color: var(--text-primary); - } -} - -/* Улучшенные стили для сетки компетенций */ -.competence-grid { - display: grid; - gap: 2rem; -} - -.competence-grid .modern-card { - padding: 0; - overflow: hidden; -} - -.competence-grid .modern-card::before { - height: 4px; - background: var(--gradient-secondary); -} - -/* Анимации появления */ -.fade-in { - animation: fadeInUp 0.8s ease-out; -} - -@keyframes fadeInUp { - from { - opacity: 0; - transform: translateY(40px); - } - to { - opacity: 1; - transform: translateY(0); - } +/* competence.css - Стили для страницы компетенций */ + +/* Основные стили для страницы компетенций */ +.competence-item { + display: flex; + gap: 2rem; + align-items: flex-start; + padding: 2rem; + background: var(--bg-card); + border-radius: var(--radius-xl); + box-shadow: var(--shadow-lg); + border-left: 4px solid var(--secondary); + transition: var(--transition); + border: 1px solid var(--border-light); +} + +.competence-item:hover { + transform: translateX(8px); + box-shadow: var(--shadow-xl); + border-color: var(--primary-light); +} + +.competence-scan-wrapper { + flex-shrink: 0; +} + +.competence-scan-container { + width: 280px; + cursor: pointer; + border-radius: var(--radius-lg); + overflow: hidden; + border: 2px solid var(--border-light); + transition: var(--transition); + position: relative; + box-shadow: var(--shadow-md); +} + +.competence-scan-container:hover { + transform: translateY(-4px) scale(1.02); + box-shadow: var(--shadow-xl); + border-color: var(--primary); +} + +.competence-scan { + width: 100%; + height: auto; + display: block; + transition: var(--transition); +} + +.competence-content { + flex: 1; + min-width: 0; +} + +.competence-title { + font-size: 1.5rem; + font-weight: 700; + color: var(--text-primary); + margin-bottom: 1rem; + background: var(--gradient-primary); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.competence-description { + line-height: 1.7; + font-size: 1.05rem; + color: var(--text-primary); +} + +.competence-description p { + margin-bottom: 1rem; +} + +.competence-description p:last-child { + margin-bottom: 0; +} + +.scan-hint { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: linear-gradient(transparent, rgba(0,0,0,0.8)); + color: white; + padding: 1rem; + text-align: center; + opacity: 0; + transition: var(--transition); + transform: translateY(10px); +} + +.competence-scan-container:hover .scan-hint { + opacity: 1; + transform: translateY(0); +} + +/* Стили для модального окна с изображением компетенций */ +.modal.competence-modal { + background-color: rgba(15, 19, 31, 0.95); + backdrop-filter: blur(10px); +} + +.modal.competence-modal .modal-content { + background: transparent; + border: none; + box-shadow: none; + max-width: 95vw; + max-height: 95vh; + margin: 2% auto; +} + +.modal.competence-modal .modal-header { + background: var(--bg-card); + border-bottom: 2px solid var(--border-light); + padding: 1.5rem 2rem; +} + +.modal.competence-modal .modal-header h3 { + margin: 0; + color: var(--text-primary); + font-size: 1.25rem; + font-weight: 600; + background: var(--gradient-primary); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.modal.competence-modal .modal-body { + padding: 1rem; + display: flex; + align-items: center; + justify-content: center; + background: transparent; +} + +.modal-image { + max-width: 90vw; + max-height: 80vh; + width: auto; + height: auto; + display: block; + margin: 0 auto; + border-radius: var(--radius-md); + box-shadow: var(--shadow-xl); + transition: all 0.3s ease; +} + +/* Анимации для модального окна */ +.modal.competence-modal { + transition: opacity 0.3s ease; + opacity: 0; +} + +.modal.competence-modal.active { + opacity: 1; +} + +.modal.competence-modal .modal-content { + transform: scale(0.7); + transition: transform 0.3s ease; +} + +.modal.competence-modal.active .modal-content { + transform: scale(1); +} + +/* Улучшенные тени и границы */ +.competence-scan-container { + border: 2px solid var(--border-light); + box-shadow: var(--shadow-lg); +} + +.competence-scan-container:hover { + border-color: var(--primary-light); + box-shadow: var(--shadow-xl); +} + +/* Адаптивность для мобильных устройств */ +@media (max-width: 768px) { + .competence-item { + flex-direction: column; + padding: 1.5rem; + gap: 1.5rem; + } + + .competence-scan-container { + width: 100%; + max-width: 300px; + margin: 0 auto; + } + + .competence-title { + font-size: 1.375rem; + text-align: center; + } + + .modal-image { + max-width: 95vw; + max-height: 70vh; + } + + .modal.competence-modal .modal-content { + margin: 10% auto; + } + + .modal.competence-modal .modal-header { + padding: 1rem 1.5rem; + } +} + +@media (max-width: 480px) { + .competence-item { + padding: 1.25rem; + } + + .competence-scan-container { + max-width: 100%; + } + + .competence-title { + font-size: 1.25rem; + } + + .modal.competence-modal .modal-content { + margin: 5% auto; + max-width: 98vw; + } + + .modal.competence-modal .modal-body { + padding: 0.5rem; + } + + .modal.competence-modal .modal-header { + padding: 1rem; + } + + .modal.competence-modal .modal-header h3 { + font-size: 1.1rem; + } +} + +/* Стили для светлой темы */ +@media (prefers-color-scheme: light) { + .modal.competence-modal { + background-color: rgba(0, 0, 0, 0.8); + } + + .modal.competence-modal .modal-header { + background: var(--bg-primary); + } + + .scan-hint { + background: linear-gradient(transparent, rgba(0,0,0,0.7)); + } + + .competence-item { + background: var(--bg-primary); + } + + .competence-description { + color: var(--text-primary); + } +} + +/* Улучшенные стили для сетки компетенций */ +.competence-grid { + display: grid; + gap: 2rem; +} + +.competence-grid .modern-card { + padding: 0; + overflow: hidden; +} + +.competence-grid .modern-card::before { + height: 4px; + background: var(--gradient-secondary); +} + +/* Анимации появления */ +.fade-in { + animation: fadeInUp 0.8s ease-out; +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(40px); + } + to { + opacity: 1; + transform: translateY(0); + } } \ No newline at end of file diff --git a/OneCprogsite/programmer/static/programmer/css/recall.css b/OneCprogsite/programmer/static/programmer/css/recall.css index b6154a8..9b7cc7a 100644 --- a/OneCprogsite/programmer/static/programmer/css/recall.css +++ b/OneCprogsite/programmer/static/programmer/css/recall.css @@ -1,228 +1,228 @@ -/* recall.css - Стили для страницы отзывов */ - -/* Основные стили для страницы отзывов */ -.recall-item { - display: flex; - flex-direction: column; - gap: 1.5rem; -} - -.recall-content { - display: flex; - gap: 2rem; - align-items: flex-start; -} - -.recall-scan-wrapper { - flex-shrink: 0; -} - -.recall-scan-container { - width: 280px; - cursor: pointer; - border-radius: var(--radius-lg); - overflow: hidden; - border: 2px solid var(--border-light); - transition: var(--transition); - position: relative; - box-shadow: var(--shadow-md); -} - -.recall-scan-container:hover { - transform: translateY(-4px) scale(1.02); - box-shadow: var(--shadow-xl); - border-color: var(--primary); -} - -.recall-scan { - width: 100%; - height: auto; - display: block; - transition: var(--transition); -} - -.recall-text { - flex: 1; - min-width: 0; - line-height: 1.7; - font-size: 1.05rem; - color: var(--text-primary); -} - -.recall-text p { - margin-bottom: 1rem; -} - -.recall-text p:last-child { - margin-bottom: 0; -} - -.scan-hint { - position: absolute; - bottom: 0; - left: 0; - right: 0; - background: linear-gradient(transparent, rgba(0,0,0,0.8)); - color: white; - padding: 1rem; - text-align: center; - opacity: 0; - transition: var(--transition); - transform: translateY(10px); -} - -.recall-scan-container:hover .scan-hint { - opacity: 1; - transform: translateY(0); -} - -/* Стили для модального окна с изображением */ -.modal.image-modal { - background-color: rgba(15, 19, 31, 0.95); - backdrop-filter: blur(10px); -} - -.modal.image-modal .modal-content { - background: transparent; - border: none; - box-shadow: none; - max-width: 95vw; - max-height: 95vh; - margin: 2% auto; -} - -.modal.image-modal .modal-header { - background: var(--bg-card); - border-bottom: 2px solid var(--border-light); - padding: 1.5rem 2rem; -} - -.modal.image-modal .modal-header h3 { - margin: 0; - color: var(--text-primary); - font-size: 1.25rem; - font-weight: 600; - background: var(--gradient-primary); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.modal.image-modal .modal-body { - padding: 1rem; - display: flex; - align-items: center; - justify-content: center; - background: transparent; -} - -.modal-image { - max-width: 90vw; - max-height: 80vh; - width: auto; - height: auto; - display: block; - margin: 0 auto; - border-radius: var(--radius-md); - box-shadow: var(--shadow-xl); - transition: all 0.3s ease; -} - -/* Анимации для модального окна */ -.modal.image-modal { - transition: opacity 0.3s ease; - opacity: 0; -} - -.modal.image-modal.active { - opacity: 1; -} - -.modal.image-modal .modal-content { - transform: scale(0.7); - transition: transform 0.3s ease; -} - -.modal.image-modal.active .modal-content { - transform: scale(1); -} - -/* Улучшенные тени и границы */ -.recall-scan-container { - border: 2px solid var(--border-light); - box-shadow: var(--shadow-lg); -} - -.recall-scan-container:hover { - border-color: var(--primary-light); - box-shadow: var(--shadow-xl); -} - -/* Адаптивность для мобильных устройств */ -@media (max-width: 768px) { - .recall-content { - flex-direction: column; - gap: 1.5rem; - } - - .recall-scan-container { - width: 100%; - max-width: 300px; - margin: 0 auto; - } - - .recall-scan-wrapper { - order: -1; - } - - .modal-image { - max-width: 95vw; - max-height: 70vh; - } - - .modal.image-modal .modal-content { - margin: 10% auto; - } - - .modal.image-modal .modal-header { - padding: 1rem 1.5rem; - } -} - -@media (max-width: 480px) { - .recall-scan-container { - max-width: 100%; - } - - .modal.image-modal .modal-content { - margin: 5% auto; - max-width: 98vw; - } - - .modal.image-modal .modal-body { - padding: 0.5rem; - } - - .modal.image-modal .modal-header { - padding: 1rem; - } - - .modal.image-modal .modal-header h3 { - font-size: 1.1rem; - } -} - -/* Стили для светлой темы */ -@media (prefers-color-scheme: light) { - .modal.image-modal { - background-color: rgba(0, 0, 0, 0.8); - } - - .modal.image-modal .modal-header { - background: var(--bg-primary); - } - - .scan-hint { - background: linear-gradient(transparent, rgba(0,0,0,0.7)); - } +/* recall.css - Стили для страницы отзывов */ + +/* Основные стили для страницы отзывов */ +.recall-item { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.recall-content { + display: flex; + gap: 2rem; + align-items: flex-start; +} + +.recall-scan-wrapper { + flex-shrink: 0; +} + +.recall-scan-container { + width: 280px; + cursor: pointer; + border-radius: var(--radius-lg); + overflow: hidden; + border: 2px solid var(--border-light); + transition: var(--transition); + position: relative; + box-shadow: var(--shadow-md); +} + +.recall-scan-container:hover { + transform: translateY(-4px) scale(1.02); + box-shadow: var(--shadow-xl); + border-color: var(--primary); +} + +.recall-scan { + width: 100%; + height: auto; + display: block; + transition: var(--transition); +} + +.recall-text { + flex: 1; + min-width: 0; + line-height: 1.7; + font-size: 1.05rem; + color: var(--text-primary); +} + +.recall-text p { + margin-bottom: 1rem; +} + +.recall-text p:last-child { + margin-bottom: 0; +} + +.scan-hint { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: linear-gradient(transparent, rgba(0,0,0,0.8)); + color: white; + padding: 1rem; + text-align: center; + opacity: 0; + transition: var(--transition); + transform: translateY(10px); +} + +.recall-scan-container:hover .scan-hint { + opacity: 1; + transform: translateY(0); +} + +/* Стили для модального окна с изображением */ +.modal.image-modal { + background-color: rgba(15, 19, 31, 0.95); + backdrop-filter: blur(10px); +} + +.modal.image-modal .modal-content { + background: transparent; + border: none; + box-shadow: none; + max-width: 95vw; + max-height: 95vh; + margin: 2% auto; +} + +.modal.image-modal .modal-header { + background: var(--bg-card); + border-bottom: 2px solid var(--border-light); + padding: 1.5rem 2rem; +} + +.modal.image-modal .modal-header h3 { + margin: 0; + color: var(--text-primary); + font-size: 1.25rem; + font-weight: 600; + background: var(--gradient-primary); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.modal.image-modal .modal-body { + padding: 1rem; + display: flex; + align-items: center; + justify-content: center; + background: transparent; +} + +.modal-image { + max-width: 90vw; + max-height: 80vh; + width: auto; + height: auto; + display: block; + margin: 0 auto; + border-radius: var(--radius-md); + box-shadow: var(--shadow-xl); + transition: all 0.3s ease; +} + +/* Анимации для модального окна */ +.modal.image-modal { + transition: opacity 0.3s ease; + opacity: 0; +} + +.modal.image-modal.active { + opacity: 1; +} + +.modal.image-modal .modal-content { + transform: scale(0.7); + transition: transform 0.3s ease; +} + +.modal.image-modal.active .modal-content { + transform: scale(1); +} + +/* Улучшенные тени и границы */ +.recall-scan-container { + border: 2px solid var(--border-light); + box-shadow: var(--shadow-lg); +} + +.recall-scan-container:hover { + border-color: var(--primary-light); + box-shadow: var(--shadow-xl); +} + +/* Адаптивность для мобильных устройств */ +@media (max-width: 768px) { + .recall-content { + flex-direction: column; + gap: 1.5rem; + } + + .recall-scan-container { + width: 100%; + max-width: 300px; + margin: 0 auto; + } + + .recall-scan-wrapper { + order: -1; + } + + .modal-image { + max-width: 95vw; + max-height: 70vh; + } + + .modal.image-modal .modal-content { + margin: 10% auto; + } + + .modal.image-modal .modal-header { + padding: 1rem 1.5rem; + } +} + +@media (max-width: 480px) { + .recall-scan-container { + max-width: 100%; + } + + .modal.image-modal .modal-content { + margin: 5% auto; + max-width: 98vw; + } + + .modal.image-modal .modal-body { + padding: 0.5rem; + } + + .modal.image-modal .modal-header { + padding: 1rem; + } + + .modal.image-modal .modal-header h3 { + font-size: 1.1rem; + } +} + +/* Стили для светлой темы */ +@media (prefers-color-scheme: light) { + .modal.image-modal { + background-color: rgba(0, 0, 0, 0.8); + } + + .modal.image-modal .modal-header { + background: var(--bg-primary); + } + + .scan-hint { + background: linear-gradient(transparent, rgba(0,0,0,0.7)); + } } \ No newline at end of file diff --git a/OneCprogsite/programmer/static/programmer/css/solution-accordion.css b/OneCprogsite/programmer/static/programmer/css/solution-accordion.css index d052032..db6d719 100644 --- a/OneCprogsite/programmer/static/programmer/css/solution-accordion.css +++ b/OneCprogsite/programmer/static/programmer/css/solution-accordion.css @@ -1,67 +1,67 @@ -/* solution-accordion.css */ -.solution-accordion { - margin: 2rem 0; -} - -.accordion-item { - background: var(--bg-primary); - border-radius: var(--radius-lg); - margin-bottom: 1rem; - border: 2px solid var(--border-light); - overflow: hidden; - transition: var(--transition); -} - -.accordion-item:hover { - border-color: var(--primary-light); -} - -.accordion-header { - padding: 1.5rem; - background: var(--gradient-primary); - color: white; - font-weight: 600; - font-size: 1.125rem; - cursor: pointer; - display: flex; - justify-content: space-between; - align-items: center; - transition: var(--transition); - user-select: none; -} - -.accordion-header:hover { - background: var(--primary-dark); -} - -.accordion-content { - padding: 0; - background: var(--bg-card); - line-height: 1.7; - color: var(--text-secondary); - max-height: 0; - overflow: hidden; - transition: all 0.3s ease; -} - -.accordion-content.active { - padding: 1.5rem; - max-height: 5000px; -} - -.accordion-icon { - transition: transform 0.3s ease; - font-size: 0.8em; -} - -.accordion-header.active .accordion-icon { - transform: rotate(180deg); -} - -.accordion-content p { - margin-bottom: 1rem; -} - -.accordion-content p:last-child { - margin-bottom: 0; +/* solution-accordion.css */ +.solution-accordion { + margin: 2rem 0; +} + +.accordion-item { + background: var(--bg-primary); + border-radius: var(--radius-lg); + margin-bottom: 1rem; + border: 2px solid var(--border-light); + overflow: hidden; + transition: var(--transition); +} + +.accordion-item:hover { + border-color: var(--primary-light); +} + +.accordion-header { + padding: 1.5rem; + background: var(--gradient-primary); + color: white; + font-weight: 600; + font-size: 1.125rem; + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + transition: var(--transition); + user-select: none; +} + +.accordion-header:hover { + background: var(--primary-dark); +} + +.accordion-content { + padding: 0; + background: var(--bg-card); + line-height: 1.7; + color: var(--text-secondary); + max-height: 0; + overflow: hidden; + transition: all 0.3s ease; +} + +.accordion-content.active { + padding: 1.5rem; + max-height: 5000px; +} + +.accordion-icon { + transition: transform 0.3s ease; + font-size: 0.8em; +} + +.accordion-header.active .accordion-icon { + transform: rotate(180deg); +} + +.accordion-content p { + margin-bottom: 1rem; +} + +.accordion-content p:last-child { + margin-bottom: 0; } \ No newline at end of file diff --git a/OneCprogsite/programmer/static/programmer/css/styles.css b/OneCprogsite/programmer/static/programmer/css/styles.css index d3347a9..1ef1d5c 100644 --- a/OneCprogsite/programmer/static/programmer/css/styles.css +++ b/OneCprogsite/programmer/static/programmer/css/styles.css @@ -1,1007 +1,1007 @@ -/* ===== CSS RESET & VARIABLES ===== */ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -:root { - /* 1C Brand Colors - Modern Gradient Palette */ - --primary: #FF6B00; /* 1C Orange */ - --primary-dark: #E55A00; - --primary-light: #FF8A3D; - --secondary: #0055A5; /* 1C Blue */ - --secondary-dark: #004488; - --secondary-light: #3377CC; - --accent: #00A8FF; - - /* Extended 1C Color Palette */ - --gradient-primary: linear-gradient(135deg, #FF6B00 0%, #FF8A3D 100%); - --gradient-secondary: linear-gradient(135deg, #0055A5 0%, #3377CC 100%); - --gradient-hero: linear-gradient(135deg, #FF6B00 0%, #0055A5 100%); - - --text-primary: #1A1F36; - --text-secondary: #4A5568; - --text-light: #718096; - - --bg-primary: #FFFFFF; - --bg-secondary: #F7FAFC; - --bg-tertiary: #EDF2F7; - - --border-light: #E2E8F0; - --border-medium: #CBD5E0; - - /* Modern Shadows */ - --shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); - --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); - --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); - --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); - - /* Border radius */ - --radius-sm: 8px; - --radius-md: 12px; - --radius-lg: 16px; - --radius-xl: 20px; - - /* Transitions */ - --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - --transition-slow: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); -} - -/* ===== BASE STYLES ===== */ -html { - scroll-behavior: smooth; -} - -body { - font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; - line-height: 1.6; - color: var(--text-primary); - background: linear-gradient(135deg, #F7FAFC 0%, #EDF2F7 100%); - font-size: 16px; - overflow-x: hidden; -} - -.container { - max-width: 1200px; - margin: 0 auto; - padding: 0 20px; -} - -/* ===== ENHANCED HEADER ===== */ -.header { - background: rgba(255, 255, 255, 0.95); - border-bottom: 1px solid var(--border-light); - box-shadow: var(--shadow-sm); - position: sticky; - top: 0; - z-index: 1000; - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); -} - -.nav { - display: flex; - align-items: center; - justify-content: space-between; - padding: 1rem 0; - gap: 2rem; -} - -.logo { - display: flex; - align-items: center; - text-decoration: none; - font-weight: 700; - font-size: 1.375rem; - color: var(--text-primary); - transition: var(--transition); - background: var(--gradient-primary); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.logo:hover { - transform: translateY(-1px); -} - -.logo-img { - width: 40px; - height: 40px; - margin-right: 12px; - border-radius: var(--radius-md); - background: var(--gradient-primary); - padding: 4px; -} - -.nav-menu { - display: flex; - list-style: none; - gap: 2.5rem; - margin: 0; - flex-wrap: wrap; -} - -.nav-link { - text-decoration: none; - color: var(--text-secondary); - font-weight: 600; - padding: 0.75rem 0; - position: relative; - transition: var(--transition); - font-size: 1rem; -} - -.nav-link:hover { - color: var(--primary); -} - -.nav-link.active { - color: var(--primary); - font-weight: 700; -} - -.nav-link.active::after { - content: ''; - position: absolute; - bottom: -2px; - left: 0; - right: 0; - height: 3px; - background: var(--gradient-primary); - border-radius: 2px; -} - -.telegram-btn { - display: flex; - align-items: center; - gap: 10px; - background: var(--gradient-primary); - color: white; - text-decoration: none; - padding: 0.875rem 1.75rem; - border-radius: var(--radius-lg); - font-weight: 600; - transition: var(--transition); - white-space: nowrap; - box-shadow: var(--shadow-md); - border: none; - cursor: pointer; - font-size: 0.95rem; -} - -.telegram-btn:hover { - transform: translateY(-2px); - box-shadow: var(--shadow-lg); - background: var(--gradient-primary); -} - -/* ===== ENHANCED HERO SECTION ===== */ -.hero-section { - text-align: center; - padding: 4rem 0 3rem; - background: var(--gradient-hero); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; - margin-bottom: 3rem; -} - -.hero-title { - font-size: 3.5rem; - font-weight: 800; - margin-bottom: 1.5rem; - line-height: 1.1; - background: var(--gradient-hero); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.hero-subtitle { - font-size: 1.375rem; - color: var(--text-secondary); - font-weight: 500; - max-width: 600px; - margin: 0 auto; - line-height: 1.6; -} - -/* ===== MODERN CARD STYLES ===== */ -.modern-card { - background: var(--bg-primary); - border-radius: var(--radius-xl); - padding: 2rem; - box-shadow: var(--shadow-lg); - border: 1px solid var(--border-light); - transition: var(--transition-slow); - margin-bottom: 2rem; - position: relative; - overflow: hidden; -} - -.modern-card::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 4px; - background: var(--gradient-primary); -} - -.modern-card:hover { - transform: translateY(-8px); - box-shadow: var(--shadow-xl); -} - -.modern-card.secondary::before { - background: var(--gradient-secondary); -} - -.card-header { - display: flex; - align-items: flex-start; - justify-content: space-between; - margin-bottom: 1.5rem; - gap: 1rem; -} - -.card-title { - font-size: 1.5rem; - font-weight: 700; - color: var(--text-primary); - margin-bottom: 0.75rem; - background: var(--gradient-primary); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.card-subtitle { - color: var(--text-secondary); - font-size: 1rem; - margin-bottom: 1.5rem; - line-height: 1.6; -} - -.card-content { - color: var(--text-primary); - line-height: 1.7; - font-size: 1.05rem; -} - -.card-content p { - margin-bottom: 1rem; -} - -.card-actions { - margin-top: 2rem; - display: flex; - gap: 1rem; - flex-wrap: wrap; -} - -/* ===== GRID LAYOUTS ===== */ -.grid { - display: grid; - gap: 2rem; -} - -.grid-2 { - grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); -} - -.grid-3 { - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); -} - -/* ===== ENHANCED BUTTONS ===== */ -.btn { - display: inline-flex; - align-items: center; - gap: 0.75rem; - padding: 1rem 2rem; - border-radius: var(--radius-lg); - text-decoration: none; - font-weight: 600; - border: none; - cursor: pointer; - transition: var(--transition); - font-size: 1rem; - position: relative; - overflow: hidden; -} - -.btn::before { - content: ''; - position: absolute; - top: 0; - left: -100%; - width: 100%; - height: 100%; - background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent); - transition: left 0.5s; -} - -.btn:hover::before { - left: 100%; -} - -.btn-primary { - background: var(--gradient-primary); - color: white; - box-shadow: var(--shadow-md); -} - -.btn-primary:hover { - transform: translateY(-2px); - box-shadow: var(--shadow-lg); -} - -.btn-secondary { - background: var(--gradient-secondary); - color: white; - box-shadow: var(--shadow-md); -} - -.btn-secondary:hover { - transform: translateY(-2px); - box-shadow: var(--shadow-lg); -} - -.btn-outline { - background: transparent; - color: var(--primary); - border: 2px solid var(--primary); -} - -.btn-outline:hover { - background: var(--primary); - color: white; - transform: translateY(-2px); -} - -/* ===== ENHANCED ABOUT PAGE ===== */ -.about-header { - text-align: center; - margin-bottom: 3rem; - padding: 2rem 0; -} - -.about-header h2 { - font-size: 2.5rem; - font-weight: 800; - margin-bottom: 0.5rem; - background: var(--gradient-hero); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.about-header .subtitle { - font-size: 1.25rem; - color: var(--text-secondary); - font-weight: 500; -} - -.about-section { - margin-bottom: 3rem; - padding: 2.5rem; - background: var(--bg-primary); - border-radius: var(--radius-xl); - box-shadow: var(--shadow-lg); - border-left: 4px solid var(--primary); -} - -.about-section h3 { - font-size: 1.5rem; - font-weight: 700; - margin-bottom: 1.5rem; - color: var(--text-primary); - display: flex; - align-items: center; - gap: 0.75rem; -} - -.skills-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); - gap: 2rem; - margin-top: 1.5rem; -} - -.skill-category { - background: var(--bg-secondary); - padding: 1.5rem; - border-radius: var(--radius-lg); - border: 1px solid var(--border-light); - transition: var(--transition); -} - -.skill-category:hover { - transform: translateY(-4px); - box-shadow: var(--shadow-md); -} - -.skill-category h4 { - color: var(--primary); - font-weight: 600; - margin-bottom: 1rem; - font-size: 1.125rem; -} - -/* ===== ENHANCED COMPETENCE STYLES ===== */ -.competence-grid { - display: grid; - gap: 2rem; -} - -.competence-item { - display: flex; - gap: 2rem; - align-items: flex-start; - padding: 2rem; - background: var(--bg-primary); - border-radius: var(--radius-xl); - box-shadow: var(--shadow-lg); - border-left: 4px solid var(--secondary); - transition: var(--transition); -} - -.competence-item:hover { - transform: translateX(8px); - box-shadow: var(--shadow-xl); -} - -.competence-scan-wrapper { - flex-shrink: 0; -} - -.competence-scan-container { - width: 220px; - cursor: pointer; - border-radius: var(--radius-lg); - overflow: hidden; - border: 2px solid var(--border-light); - transition: var(--transition); - position: relative; - box-shadow: var(--shadow-md); -} - -.competence-scan-container:hover { - transform: translateY(-4px) scale(1.02); - box-shadow: var(--shadow-xl); - border-color: var(--primary); -} - -.competence-scan { - width: 100%; - height: auto; - display: block; - transition: var(--transition); -} - -.scan-hint { - position: absolute; - bottom: 0; - left: 0; - right: 0; - background: linear-gradient(transparent, rgba(0,0,0,0.8)); - color: white; - padding: 1rem; - text-align: center; - opacity: 0; - transition: var(--transition); - transform: translateY(10px); -} - -.competence-scan-container:hover .scan-hint { - opacity: 1; - transform: translateY(0); -} - -/* ===== ENHANCED RECALL STYLES ===== */ -.recall-grid { - display: grid; - gap: 2rem; -} - -.recall-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - margin-bottom: 1.5rem; - padding-bottom: 1.5rem; - border-bottom: 2px solid var(--border-light); -} - -.recall-title { - font-size: 1.375rem; - font-weight: 700; - color: var(--text-primary); - margin-bottom: 0.75rem; -} - -.recall-meta { - display: flex; - gap: 1rem; - font-size: 0.875rem; - font-weight: 500; -} - -.recall-date { - background: var(--gradient-primary); - color: white; - padding: 0.5rem 1rem; - border-radius: var(--radius-md); - font-weight: 600; -} - -.recall-client { - background: var(--gradient-secondary); - color: white; - padding: 0.5rem 1rem; - border-radius: var(--radius-md); - font-weight: 600; -} - -/* ===== ENHANCED PAGE HEADERS ===== */ -.page-header { - margin-bottom: 3rem; - padding: 3rem 0; - text-align: center; - background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%); - border-radius: var(--radius-xl); - box-shadow: var(--shadow-md); - border: 1px solid var(--border-light); -} - -.page-title { - font-size: 3rem; - font-weight: 800; - color: var(--text-primary); - margin-bottom: 1rem; - background: var(--gradient-hero); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.page-subtitle { - color: var(--text-secondary); - font-size: 1.25rem; - font-weight: 500; - max-width: 600px; - margin: 0 auto; -} - -/* ===== ENHANCED FOOTER ===== */ -.footer { - background: linear-gradient(135deg, var(--text-primary) 0%, #2D3748 100%); - color: white; - padding: 4rem 0 2rem; - margin-top: 6rem; - position: relative; -} - -.footer::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 4px; - background: var(--gradient-primary); -} - -.footer-content { - display: grid; - grid-template-columns: 1fr 1fr 1fr; - gap: 3rem; - margin-bottom: 3rem; -} - -.footer-info h3 { - margin-bottom: 1rem; - color: white; - font-size: 1.5rem; - font-weight: 700; -} - -.footer-info p { - opacity: 0.9; - font-size: 1.05rem; -} - -.footer-contacts p { - margin-bottom: 0.75rem; - opacity: 0.9; - display: flex; - align-items: center; - gap: 0.5rem; - font-size: 1.05rem; -} - -.footer-copyright { - grid-column: 1 / -1; - text-align: center; - padding-top: 2rem; - border-top: 1px solid rgba(255, 255, 255, 0.2); - opacity: 0.7; - font-size: 0.95rem; -} - -/* ===== ANIMATIONS ===== */ -@keyframes fadeInUp { - from { - opacity: 0; - transform: translateY(40px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -@keyframes float { - 0%, 100% { - transform: translateY(0); - } - 50% { - transform: translateY(-10px); - } -} - -@keyframes gradientShift { - 0% { - background-position: 0% 50%; - } - 50% { - background-position: 100% 50%; - } - 100% { - background-position: 0% 50%; - } -} - -.fade-in { - animation: fadeInUp 0.8s ease-out; -} - -.float { - animation: float 3s ease-in-out infinite; -} - -.gradient-animate { - background-size: 200% 200%; - animation: gradientShift 3s ease infinite; -} - -/* ===== RESPONSIVE DESIGN ===== */ -@media (max-width: 1024px) { - .hero-title { - font-size: 3rem; - } - - .grid-2 { - grid-template-columns: 1fr; - } - - .footer-content { - grid-template-columns: 1fr 1fr; - } -} - -@media (max-width: 768px) { - .container { - padding: 0 16px; - } - - .nav { - flex-direction: column; - gap: 1.5rem; - text-align: center; - } - - .nav-menu { - justify-content: center; - gap: 1.5rem; - } - - .hero-title { - font-size: 2.5rem; - } - - .hero-subtitle { - font-size: 1.125rem; - } - - .competence-item { - flex-direction: column; - text-align: center; - gap: 1.5rem; - } - - .competence-scan-container { - width: 100%; - max-width: 280px; - margin: 0 auto; - } - - .footer-content { - grid-template-columns: 1fr; - text-align: center; - gap: 2rem; - } - - .page-content { - padding: 1.5rem; - } - - .about-section { - padding: 2rem 1.5rem; - } -} - -@media (max-width: 480px) { - .main { - padding: 1rem 0; - } - - .page-content { - padding: 1rem; - } - - .modern-card { - padding: 1.5rem; - } - - .card-actions { - flex-direction: column; - } - - .btn { - width: 100%; - justify-content: center; - } - - .nav-menu { - flex-direction: column; - gap: 1rem; - } - - .hero-title { - font-size: 2rem; - } - - .page-title { - font-size: 2rem; - } -} - -/* ===== UTILITY CLASSES ===== */ -.text-center { text-align: center; } -.text-left { text-align: left; } -.text-right { text-align: right; } - -.justify-center { justify-content: center; } -.justify-between { justify-content: space-between; } - -.mb-1 { margin-bottom: 0.5rem; } -.mb-2 { margin-bottom: 1rem; } -.mb-3 { margin-bottom: 1.5rem; } -.mb-4 { margin-bottom: 2rem; } - -.mt-1 { margin-top: 0.5rem; } -.mt-2 { margin-top: 1rem; } -.mt-3 { margin-top: 1.5rem; } -.mt-4 { margin-top: 2rem; } - -.hidden { display: none; } - -/* ===== CUSTOM COMPONENTS ===== */ -.content-card { - background: var(--bg-primary); - border-radius: var(--radius-xl); - padding: 2.5rem; - box-shadow: var(--shadow-lg); - border: 1px solid var(--border-light); - margin-bottom: 2rem; - position: relative; - overflow: hidden; -} - -.content-card::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 4px; - background: var(--gradient-primary); -} - -.improved-list { - list-style: none; - padding: 0; -} - -.improved-list li { - margin-bottom: 2rem; -} - -/* ===== FORM STYLES ===== */ -.form-group { - margin-bottom: 1.5rem; -} - -.form-group label { - display: block; - margin-bottom: 0.5rem; - font-weight: 600; - color: var(--text-primary); -} - -.form-input, .form-textarea { - width: 100%; - padding: 1rem 1.25rem; - border: 2px solid var(--border-light); - border-radius: var(--radius-lg); - font-size: 1rem; - transition: var(--transition); - background: var(--bg-primary); - font-family: inherit; -} - -.form-input:focus, .form-textarea:focus { - outline: none; - border-color: var(--primary); - box-shadow: 0 0 0 4px rgba(255, 107, 0, 0.1); - transform: translateY(-2px); -} - -.form-textarea { - resize: vertical; - min-height: 120px; - line-height: 1.5; -} - -/* ===== MODAL ENHANCEMENTS ===== */ -.modal { - display: none; - position: fixed; - z-index: 1000; - left: 0; - top: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.8); - backdrop-filter: blur(10px); - animation: fadeIn 0.3s ease; -} - -.modal-content { - background: var(--bg-primary); - margin: 2% auto; - border-radius: var(--radius-xl); - box-shadow: var(--shadow-xl); - animation: slideIn 0.3s ease; - position: relative; - max-width: 90%; - max-height: 90vh; - overflow: auto; -} - -.modal-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 2rem 2rem 1rem; - border-bottom: 2px solid var(--border-light); -} - -.modal-header h3 { - margin: 0; - color: var(--text-primary); - font-size: 1.5rem; - font-weight: 700; - background: var(--gradient-primary); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.modal-body { - padding: 2rem; -} - -.modal-close { - color: var(--text-light); - font-size: 2rem; - font-weight: bold; - cursor: pointer; - transition: var(--transition); - background: none; - border: none; - padding: 0; - width: 40px; - height: 40px; - display: flex; - align-items: center; - justify-content: center; - border-radius: var(--radius-md); -} - -.modal-close:hover { - color: var(--primary); - background: var(--bg-secondary); - transform: rotate(90deg); -} - -@keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } -} - -@keyframes slideIn { - from { - opacity: 0; - transform: translateY(-50px) scale(0.9); - } - to { - opacity: 1; - transform: translateY(0) scale(1); - } -} - -/* ===== SOLUTION PAGE STYLES ===== */ -.solution-accordion { - margin: 2rem 0; -} - -.accordion-item { - background: var(--bg-secondary); - border-radius: var(--radius-lg); - margin-bottom: 1rem; - border: 2px solid var(--border-light); - overflow: hidden; - transition: var(--transition); -} - -.accordion-item:hover { - border-color: var(--primary-light); - transform: translateX(4px); -} - -.accordion-header { - padding: 1.5rem; - background: var(--gradient-primary); - color: white; - font-weight: 600; - font-size: 1.125rem; - cursor: pointer; - display: flex; - justify-content: space-between; - align-items: center; - transition: var(--transition); -} - -.accordion-header:hover { - background: var(--primary-dark); -} - -.accordion-content { - padding: 1.5rem; - background: var(--bg-primary); - line-height: 1.7; -} - -.accordion-content p { - margin-bottom: 1rem; -} - -.accordion-content p:last-child { - margin-bottom: 0; +/* ===== CSS RESET & VARIABLES ===== */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + /* 1C Brand Colors - Modern Gradient Palette */ + --primary: #FF6B00; /* 1C Orange */ + --primary-dark: #E55A00; + --primary-light: #FF8A3D; + --secondary: #0055A5; /* 1C Blue */ + --secondary-dark: #004488; + --secondary-light: #3377CC; + --accent: #00A8FF; + + /* Extended 1C Color Palette */ + --gradient-primary: linear-gradient(135deg, #FF6B00 0%, #FF8A3D 100%); + --gradient-secondary: linear-gradient(135deg, #0055A5 0%, #3377CC 100%); + --gradient-hero: linear-gradient(135deg, #FF6B00 0%, #0055A5 100%); + + --text-primary: #1A1F36; + --text-secondary: #4A5568; + --text-light: #718096; + + --bg-primary: #FFFFFF; + --bg-secondary: #F7FAFC; + --bg-tertiary: #EDF2F7; + + --border-light: #E2E8F0; + --border-medium: #CBD5E0; + + /* Modern Shadows */ + --shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); + + /* Border radius */ + --radius-sm: 8px; + --radius-md: 12px; + --radius-lg: 16px; + --radius-xl: 20px; + + /* Transitions */ + --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + --transition-slow: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); +} + +/* ===== BASE STYLES ===== */ +html { + scroll-behavior: smooth; +} + +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + line-height: 1.6; + color: var(--text-primary); + background: linear-gradient(135deg, #F7FAFC 0%, #EDF2F7 100%); + font-size: 16px; + overflow-x: hidden; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 20px; +} + +/* ===== ENHANCED HEADER ===== */ +.header { + background: rgba(255, 255, 255, 0.95); + border-bottom: 1px solid var(--border-light); + box-shadow: var(--shadow-sm); + position: sticky; + top: 0; + z-index: 1000; + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); +} + +.nav { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem 0; + gap: 2rem; +} + +.logo { + display: flex; + align-items: center; + text-decoration: none; + font-weight: 700; + font-size: 1.375rem; + color: var(--text-primary); + transition: var(--transition); + background: var(--gradient-primary); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.logo:hover { + transform: translateY(-1px); +} + +.logo-img { + width: 40px; + height: 40px; + margin-right: 12px; + border-radius: var(--radius-md); + background: var(--gradient-primary); + padding: 4px; +} + +.nav-menu { + display: flex; + list-style: none; + gap: 2.5rem; + margin: 0; + flex-wrap: wrap; +} + +.nav-link { + text-decoration: none; + color: var(--text-secondary); + font-weight: 600; + padding: 0.75rem 0; + position: relative; + transition: var(--transition); + font-size: 1rem; +} + +.nav-link:hover { + color: var(--primary); +} + +.nav-link.active { + color: var(--primary); + font-weight: 700; +} + +.nav-link.active::after { + content: ''; + position: absolute; + bottom: -2px; + left: 0; + right: 0; + height: 3px; + background: var(--gradient-primary); + border-radius: 2px; +} + +.telegram-btn { + display: flex; + align-items: center; + gap: 10px; + background: var(--gradient-primary); + color: white; + text-decoration: none; + padding: 0.875rem 1.75rem; + border-radius: var(--radius-lg); + font-weight: 600; + transition: var(--transition); + white-space: nowrap; + box-shadow: var(--shadow-md); + border: none; + cursor: pointer; + font-size: 0.95rem; +} + +.telegram-btn:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-lg); + background: var(--gradient-primary); +} + +/* ===== ENHANCED HERO SECTION ===== */ +.hero-section { + text-align: center; + padding: 4rem 0 3rem; + background: var(--gradient-hero); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + margin-bottom: 3rem; +} + +.hero-title { + font-size: 3.5rem; + font-weight: 800; + margin-bottom: 1.5rem; + line-height: 1.1; + background: var(--gradient-hero); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.hero-subtitle { + font-size: 1.375rem; + color: var(--text-secondary); + font-weight: 500; + max-width: 600px; + margin: 0 auto; + line-height: 1.6; +} + +/* ===== MODERN CARD STYLES ===== */ +.modern-card { + background: var(--bg-primary); + border-radius: var(--radius-xl); + padding: 2rem; + box-shadow: var(--shadow-lg); + border: 1px solid var(--border-light); + transition: var(--transition-slow); + margin-bottom: 2rem; + position: relative; + overflow: hidden; +} + +.modern-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: var(--gradient-primary); +} + +.modern-card:hover { + transform: translateY(-8px); + box-shadow: var(--shadow-xl); +} + +.modern-card.secondary::before { + background: var(--gradient-secondary); +} + +.card-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + margin-bottom: 1.5rem; + gap: 1rem; +} + +.card-title { + font-size: 1.5rem; + font-weight: 700; + color: var(--text-primary); + margin-bottom: 0.75rem; + background: var(--gradient-primary); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.card-subtitle { + color: var(--text-secondary); + font-size: 1rem; + margin-bottom: 1.5rem; + line-height: 1.6; +} + +.card-content { + color: var(--text-primary); + line-height: 1.7; + font-size: 1.05rem; +} + +.card-content p { + margin-bottom: 1rem; +} + +.card-actions { + margin-top: 2rem; + display: flex; + gap: 1rem; + flex-wrap: wrap; +} + +/* ===== GRID LAYOUTS ===== */ +.grid { + display: grid; + gap: 2rem; +} + +.grid-2 { + grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); +} + +.grid-3 { + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); +} + +/* ===== ENHANCED BUTTONS ===== */ +.btn { + display: inline-flex; + align-items: center; + gap: 0.75rem; + padding: 1rem 2rem; + border-radius: var(--radius-lg); + text-decoration: none; + font-weight: 600; + border: none; + cursor: pointer; + transition: var(--transition); + font-size: 1rem; + position: relative; + overflow: hidden; +} + +.btn::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent); + transition: left 0.5s; +} + +.btn:hover::before { + left: 100%; +} + +.btn-primary { + background: var(--gradient-primary); + color: white; + box-shadow: var(--shadow-md); +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-lg); +} + +.btn-secondary { + background: var(--gradient-secondary); + color: white; + box-shadow: var(--shadow-md); +} + +.btn-secondary:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-lg); +} + +.btn-outline { + background: transparent; + color: var(--primary); + border: 2px solid var(--primary); +} + +.btn-outline:hover { + background: var(--primary); + color: white; + transform: translateY(-2px); +} + +/* ===== ENHANCED ABOUT PAGE ===== */ +.about-header { + text-align: center; + margin-bottom: 3rem; + padding: 2rem 0; +} + +.about-header h2 { + font-size: 2.5rem; + font-weight: 800; + margin-bottom: 0.5rem; + background: var(--gradient-hero); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.about-header .subtitle { + font-size: 1.25rem; + color: var(--text-secondary); + font-weight: 500; +} + +.about-section { + margin-bottom: 3rem; + padding: 2.5rem; + background: var(--bg-primary); + border-radius: var(--radius-xl); + box-shadow: var(--shadow-lg); + border-left: 4px solid var(--primary); +} + +.about-section h3 { + font-size: 1.5rem; + font-weight: 700; + margin-bottom: 1.5rem; + color: var(--text-primary); + display: flex; + align-items: center; + gap: 0.75rem; +} + +.skills-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 2rem; + margin-top: 1.5rem; +} + +.skill-category { + background: var(--bg-secondary); + padding: 1.5rem; + border-radius: var(--radius-lg); + border: 1px solid var(--border-light); + transition: var(--transition); +} + +.skill-category:hover { + transform: translateY(-4px); + box-shadow: var(--shadow-md); +} + +.skill-category h4 { + color: var(--primary); + font-weight: 600; + margin-bottom: 1rem; + font-size: 1.125rem; +} + +/* ===== ENHANCED COMPETENCE STYLES ===== */ +.competence-grid { + display: grid; + gap: 2rem; +} + +.competence-item { + display: flex; + gap: 2rem; + align-items: flex-start; + padding: 2rem; + background: var(--bg-primary); + border-radius: var(--radius-xl); + box-shadow: var(--shadow-lg); + border-left: 4px solid var(--secondary); + transition: var(--transition); +} + +.competence-item:hover { + transform: translateX(8px); + box-shadow: var(--shadow-xl); +} + +.competence-scan-wrapper { + flex-shrink: 0; +} + +.competence-scan-container { + width: 220px; + cursor: pointer; + border-radius: var(--radius-lg); + overflow: hidden; + border: 2px solid var(--border-light); + transition: var(--transition); + position: relative; + box-shadow: var(--shadow-md); +} + +.competence-scan-container:hover { + transform: translateY(-4px) scale(1.02); + box-shadow: var(--shadow-xl); + border-color: var(--primary); +} + +.competence-scan { + width: 100%; + height: auto; + display: block; + transition: var(--transition); +} + +.scan-hint { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: linear-gradient(transparent, rgba(0,0,0,0.8)); + color: white; + padding: 1rem; + text-align: center; + opacity: 0; + transition: var(--transition); + transform: translateY(10px); +} + +.competence-scan-container:hover .scan-hint { + opacity: 1; + transform: translateY(0); +} + +/* ===== ENHANCED RECALL STYLES ===== */ +.recall-grid { + display: grid; + gap: 2rem; +} + +.recall-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 1.5rem; + padding-bottom: 1.5rem; + border-bottom: 2px solid var(--border-light); +} + +.recall-title { + font-size: 1.375rem; + font-weight: 700; + color: var(--text-primary); + margin-bottom: 0.75rem; +} + +.recall-meta { + display: flex; + gap: 1rem; + font-size: 0.875rem; + font-weight: 500; +} + +.recall-date { + background: var(--gradient-primary); + color: white; + padding: 0.5rem 1rem; + border-radius: var(--radius-md); + font-weight: 600; +} + +.recall-client { + background: var(--gradient-secondary); + color: white; + padding: 0.5rem 1rem; + border-radius: var(--radius-md); + font-weight: 600; +} + +/* ===== ENHANCED PAGE HEADERS ===== */ +.page-header { + margin-bottom: 3rem; + padding: 3rem 0; + text-align: center; + background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%); + border-radius: var(--radius-xl); + box-shadow: var(--shadow-md); + border: 1px solid var(--border-light); +} + +.page-title { + font-size: 3rem; + font-weight: 800; + color: var(--text-primary); + margin-bottom: 1rem; + background: var(--gradient-hero); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.page-subtitle { + color: var(--text-secondary); + font-size: 1.25rem; + font-weight: 500; + max-width: 600px; + margin: 0 auto; +} + +/* ===== ENHANCED FOOTER ===== */ +.footer { + background: linear-gradient(135deg, var(--text-primary) 0%, #2D3748 100%); + color: white; + padding: 4rem 0 2rem; + margin-top: 6rem; + position: relative; +} + +.footer::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: var(--gradient-primary); +} + +.footer-content { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 3rem; + margin-bottom: 3rem; +} + +.footer-info h3 { + margin-bottom: 1rem; + color: white; + font-size: 1.5rem; + font-weight: 700; +} + +.footer-info p { + opacity: 0.9; + font-size: 1.05rem; +} + +.footer-contacts p { + margin-bottom: 0.75rem; + opacity: 0.9; + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 1.05rem; +} + +.footer-copyright { + grid-column: 1 / -1; + text-align: center; + padding-top: 2rem; + border-top: 1px solid rgba(255, 255, 255, 0.2); + opacity: 0.7; + font-size: 0.95rem; +} + +/* ===== ANIMATIONS ===== */ +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(40px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes float { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-10px); + } +} + +@keyframes gradientShift { + 0% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } + 100% { + background-position: 0% 50%; + } +} + +.fade-in { + animation: fadeInUp 0.8s ease-out; +} + +.float { + animation: float 3s ease-in-out infinite; +} + +.gradient-animate { + background-size: 200% 200%; + animation: gradientShift 3s ease infinite; +} + +/* ===== RESPONSIVE DESIGN ===== */ +@media (max-width: 1024px) { + .hero-title { + font-size: 3rem; + } + + .grid-2 { + grid-template-columns: 1fr; + } + + .footer-content { + grid-template-columns: 1fr 1fr; + } +} + +@media (max-width: 768px) { + .container { + padding: 0 16px; + } + + .nav { + flex-direction: column; + gap: 1.5rem; + text-align: center; + } + + .nav-menu { + justify-content: center; + gap: 1.5rem; + } + + .hero-title { + font-size: 2.5rem; + } + + .hero-subtitle { + font-size: 1.125rem; + } + + .competence-item { + flex-direction: column; + text-align: center; + gap: 1.5rem; + } + + .competence-scan-container { + width: 100%; + max-width: 280px; + margin: 0 auto; + } + + .footer-content { + grid-template-columns: 1fr; + text-align: center; + gap: 2rem; + } + + .page-content { + padding: 1.5rem; + } + + .about-section { + padding: 2rem 1.5rem; + } +} + +@media (max-width: 480px) { + .main { + padding: 1rem 0; + } + + .page-content { + padding: 1rem; + } + + .modern-card { + padding: 1.5rem; + } + + .card-actions { + flex-direction: column; + } + + .btn { + width: 100%; + justify-content: center; + } + + .nav-menu { + flex-direction: column; + gap: 1rem; + } + + .hero-title { + font-size: 2rem; + } + + .page-title { + font-size: 2rem; + } +} + +/* ===== UTILITY CLASSES ===== */ +.text-center { text-align: center; } +.text-left { text-align: left; } +.text-right { text-align: right; } + +.justify-center { justify-content: center; } +.justify-between { justify-content: space-between; } + +.mb-1 { margin-bottom: 0.5rem; } +.mb-2 { margin-bottom: 1rem; } +.mb-3 { margin-bottom: 1.5rem; } +.mb-4 { margin-bottom: 2rem; } + +.mt-1 { margin-top: 0.5rem; } +.mt-2 { margin-top: 1rem; } +.mt-3 { margin-top: 1.5rem; } +.mt-4 { margin-top: 2rem; } + +.hidden { display: none; } + +/* ===== CUSTOM COMPONENTS ===== */ +.content-card { + background: var(--bg-primary); + border-radius: var(--radius-xl); + padding: 2.5rem; + box-shadow: var(--shadow-lg); + border: 1px solid var(--border-light); + margin-bottom: 2rem; + position: relative; + overflow: hidden; +} + +.content-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: var(--gradient-primary); +} + +.improved-list { + list-style: none; + padding: 0; +} + +.improved-list li { + margin-bottom: 2rem; +} + +/* ===== FORM STYLES ===== */ +.form-group { + margin-bottom: 1.5rem; +} + +.form-group label { + display: block; + margin-bottom: 0.5rem; + font-weight: 600; + color: var(--text-primary); +} + +.form-input, .form-textarea { + width: 100%; + padding: 1rem 1.25rem; + border: 2px solid var(--border-light); + border-radius: var(--radius-lg); + font-size: 1rem; + transition: var(--transition); + background: var(--bg-primary); + font-family: inherit; +} + +.form-input:focus, .form-textarea:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 4px rgba(255, 107, 0, 0.1); + transform: translateY(-2px); +} + +.form-textarea { + resize: vertical; + min-height: 120px; + line-height: 1.5; +} + +/* ===== MODAL ENHANCEMENTS ===== */ +.modal { + display: none; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.8); + backdrop-filter: blur(10px); + animation: fadeIn 0.3s ease; +} + +.modal-content { + background: var(--bg-primary); + margin: 2% auto; + border-radius: var(--radius-xl); + box-shadow: var(--shadow-xl); + animation: slideIn 0.3s ease; + position: relative; + max-width: 90%; + max-height: 90vh; + overflow: auto; +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 2rem 2rem 1rem; + border-bottom: 2px solid var(--border-light); +} + +.modal-header h3 { + margin: 0; + color: var(--text-primary); + font-size: 1.5rem; + font-weight: 700; + background: var(--gradient-primary); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.modal-body { + padding: 2rem; +} + +.modal-close { + color: var(--text-light); + font-size: 2rem; + font-weight: bold; + cursor: pointer; + transition: var(--transition); + background: none; + border: none; + padding: 0; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-md); +} + +.modal-close:hover { + color: var(--primary); + background: var(--bg-secondary); + transform: rotate(90deg); +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(-50px) scale(0.9); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +/* ===== SOLUTION PAGE STYLES ===== */ +.solution-accordion { + margin: 2rem 0; +} + +.accordion-item { + background: var(--bg-secondary); + border-radius: var(--radius-lg); + margin-bottom: 1rem; + border: 2px solid var(--border-light); + overflow: hidden; + transition: var(--transition); +} + +.accordion-item:hover { + border-color: var(--primary-light); + transform: translateX(4px); +} + +.accordion-header { + padding: 1.5rem; + background: var(--gradient-primary); + color: white; + font-weight: 600; + font-size: 1.125rem; + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + transition: var(--transition); +} + +.accordion-header:hover { + background: var(--primary-dark); +} + +.accordion-content { + padding: 1.5rem; + background: var(--bg-primary); + line-height: 1.7; +} + +.accordion-content p { + margin-bottom: 1rem; +} + +.accordion-content p:last-child { + margin-bottom: 0; } \ No newline at end of file diff --git a/OneCprogsite/programmer/static/programmer/css/styles_dark.css b/OneCprogsite/programmer/static/programmer/css/styles_dark.css index b4b07c3..5be643e 100644 --- a/OneCprogsite/programmer/static/programmer/css/styles_dark.css +++ b/OneCprogsite/programmer/static/programmer/css/styles_dark.css @@ -1,1473 +1,1473 @@ -/* ===== CSS RESET & VARIABLES ===== */ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -:root { - /* 1C Brand Colors - Modern Gradient Palette */ - --primary: #FF6B00; /* 1C Orange */ - --primary-dark: #E55A00; - --primary-light: #FF8A3D; - --secondary: #0055A5; /* 1C Blue */ - --secondary-dark: #004488; - --secondary-light: #3377CC; - --accent: #00A8FF; - - /* Extended 1C Color Palette */ - --gradient-primary: linear-gradient(135deg, #FF6B00 0%, #FF8A3D 100%); - --gradient-secondary: linear-gradient(135deg, #0055A5 0%, #3377CC 100%); - --gradient-hero: linear-gradient(135deg, #FF6B00 0%, #0055A5 100%); - - /* Dark Theme Colors */ - --bg-primary: #1A1F2E; - --bg-secondary: #151925; - --bg-tertiary: #0F131F; - --bg-card: #222738; - - --text-primary: #FFFFFF; - --text-secondary: #B0B8D1; - --text-light: #8A93B0; - - --border-light: #2D3447; - --border-medium: #3A4158; - --border-dark: #252A3A; - - /* Modern Shadows */ - --shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.3), 0 1px 2px 0 rgba(0, 0, 0, 0.2); - --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -1px rgba(0, 0, 0, 0.3); - --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.5), 0 4px 6px -2px rgba(0, 0, 0, 0.4); - --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.6), 0 10px 10px -5px rgba(0, 0, 0, 0.5); - - /* Border radius */ - --radius-sm: 8px; - --radius-md: 12px; - --radius-lg: 16px; - --radius-xl: 20px; - - /* Transitions */ - --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - --transition-slow: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); -} - -/* ===== BASE STYLES ===== */ -html { - scroll-behavior: smooth; -} - -body { - font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; - line-height: 1.6; - color: var(--text-primary); - background: linear-gradient(135deg, var(--bg-tertiary) 0%, var(--bg-secondary) 50%, var(--bg-primary) 100%); - font-size: 16px; - overflow-x: hidden; - min-height: 100vh; -} - -.container { - max-width: 1200px; - margin: 0 auto; - padding: 0 20px; -} - -/* ===== ENHANCED HEADER ===== */ -.header { - background: rgba(26, 31, 46, 0.95); - border-bottom: 1px solid var(--border-dark); - box-shadow: var(--shadow-lg); - position: sticky; - top: 0; - z-index: 1000; - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); -} - -.nav { - display: flex; - align-items: center; - justify-content: space-between; - padding: 1rem 0; - gap: 2rem; -} - -.logo { - display: flex; - align-items: center; - text-decoration: none; - font-weight: 700; - font-size: 1.375rem; - color: var(--text-primary); - transition: var(--transition); - background: var(--gradient-primary); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.logo:hover { - transform: translateY(-1px); -} - -.logo-img { - width: 40px; - height: 40px; - margin-right: 12px; - border-radius: var(--radius-md); - background: var(--gradient-primary); - padding: 4px; - filter: drop-shadow(0 4px 8px rgba(255, 107, 0, 0.3)); -} - -.nav-menu { - display: flex; - list-style: none; - gap: 2.5rem; - margin: 0; - flex-wrap: wrap; -} - -.nav-link { - text-decoration: none; - color: var(--text-secondary); - font-weight: 600; - padding: 0.75rem 0; - position: relative; - transition: var(--transition); - font-size: 1rem; -} - -.nav-link:hover { - color: var(--primary); -} - -.nav-link.active { - color: var(--primary); - font-weight: 700; -} - -.nav-link.active::after { - content: ''; - position: absolute; - bottom: -2px; - left: 0; - right: 0; - height: 3px; - background: var(--gradient-primary); - border-radius: 2px; -} - -.telegram-btn { - display: flex; - align-items: center; - gap: 10px; - background: var(--gradient-primary); - color: white; - text-decoration: none; - padding: 0.6rem 1.2rem; - border-radius: var(--radius-lg); - font-weight: 600; - transition: var(--transition); - white-space: nowrap; - box-shadow: var(--shadow-md); - border: none; - cursor: pointer; - font-size: 0.95rem; -} - -.telegram-btn:hover { - transform: translateY(-2px); - box-shadow: var(--shadow-lg); - background: var(--gradient-primary); -} - -/* ===== ENHANCED HERO SECTION ===== */ -.hero-section { - text-align: center; - padding: 5rem 0 4rem; - margin-bottom: 3rem; - position: relative; -} - -.hero-section::before { - content: ''; - position: absolute; - top: 0; - left: 50%; - transform: translateX(-50%); - width: 200px; - height: 200px; - background: radial-gradient(circle, rgba(255, 107, 0, 0.2) 0%, transparent 70%); - border-radius: 50%; -} - -.hero-title { - font-size: 3.5rem; - font-weight: 800; - margin-bottom: 1.5rem; - line-height: 1.1; - background: var(--gradient-hero); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; - text-shadow: 0 4px 20px rgba(255, 107, 0, 0.3); -} - -.hero-subtitle { - font-size: 1.375rem; - color: var(--text-secondary); - font-weight: 500; - max-width: 600px; - margin: 0 auto; - line-height: 1.6; -} - -/* ===== MODERN CARD STYLES ===== */ -.modern-card { - background: var(--bg-card); - border-radius: var(--radius-xl); - padding: 2rem; - box-shadow: var(--shadow-lg); - border: 1px solid var(--border-light); - transition: var(--transition-slow); - margin-bottom: 2rem; - position: relative; - overflow: hidden; -} - -.modern-card::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 4px; - background: var(--gradient-primary); -} - -.modern-card:hover { - transform: translateY(-8px); - box-shadow: var(--shadow-xl); - border-color: var(--primary-light); -} - -.modern-card.secondary::before { - background: var(--gradient-secondary); -} - -.card-header { - display: flex; - align-items: flex-start; - justify-content: space-between; - margin-bottom: 1.5rem; - gap: 1rem; -} - -.card-title { - font-size: 1.5rem; - font-weight: 700; - color: var(--text-primary); - margin-bottom: 0.75rem; - background: var(--gradient-primary); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.card-subtitle { - color: var(--text-secondary); - font-size: 1rem; - margin-bottom: 1.5rem; - line-height: 1.6; -} - -.card-content { - color: var(--text-primary); - line-height: 1.7; - font-size: 1.05rem; -} - -.card-content p { - margin-bottom: 1rem; -} - -.card-actions { - margin-top: 2rem; - display: flex; - gap: 1rem; - flex-wrap: wrap; -} - -/* ===== GRID LAYOUTS ===== */ -.grid { - display: grid; - gap: 2rem; -} - -.grid-2 { - grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); -} - -.grid-3 { - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); -} - -/* ===== ENHANCED BUTTONS ===== */ -.btn { - display: inline-flex; - align-items: center; - gap: 0.75rem; - padding: 1rem 2rem; - border-radius: var(--radius-lg); - text-decoration: none; - font-weight: 600; - border: none; - cursor: pointer; - transition: var(--transition); - font-size: 1rem; - position: relative; - overflow: hidden; -} - -.btn::before { - content: ''; - position: absolute; - top: 0; - left: -100%; - width: 100%; - height: 100%; - background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent); - transition: left 0.5s; -} - -.btn:hover::before { - left: 100%; -} - -.btn-primary { - background: var(--gradient-primary); - color: white; - box-shadow: var(--shadow-md); -} - -.btn-primary:hover { - transform: translateY(-2px); - box-shadow: var(--shadow-lg); -} - -.btn-secondary { - background: var(--gradient-secondary); - color: white; - box-shadow: var(--shadow-md); -} - -.btn-secondary:hover { - transform: translateY(-2px); - box-shadow: var(--shadow-lg); -} - -.btn-outline { - background: transparent; - color: var(--primary); - border: 2px solid var(--primary); -} - -.btn-outline:hover { - background: var(--primary); - color: white; - transform: translateY(-2px); -} - -/* ===== ENHANCED ABOUT PAGE ===== */ -.about-header { - text-align: center; - margin-bottom: 3rem; - padding: 3rem 0; - background: linear-gradient(135deg, var(--bg-card) 0%, var(--bg-primary) 100%); - border-radius: var(--radius-xl); - box-shadow: var(--shadow-lg); - border: 1px solid var(--border-light); -} - -.about-header h2 { - font-size: 2.5rem; - font-weight: 800; - margin-bottom: 0.5rem; - background: var(--gradient-hero); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.about-header .subtitle { - font-size: 1.25rem; - color: var(--text-secondary); - font-weight: 500; -} - -.about-section { - margin-bottom: 3rem; - padding: 2.5rem; - background: var(--bg-card); - border-radius: var(--radius-xl); - box-shadow: var(--shadow-lg); - border-left: 4px solid var(--primary); - border: 1px solid var(--border-light); -} - -.about-section h3 { - font-size: 1.5rem; - font-weight: 700; - margin-bottom: 1.5rem; - color: var(--text-primary); - display: flex; - align-items: center; - gap: 0.75rem; -} - -.skills-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); - gap: 2rem; - margin-top: 1.5rem; -} - -.skill-category { - background: var(--bg-primary); - padding: 1.5rem; - border-radius: var(--radius-lg); - border: 1px solid var(--border-light); - transition: var(--transition); -} - -.skill-category:hover { - transform: translateY(-4px); - box-shadow: var(--shadow-md); - border-color: var(--primary-light); -} - -.skill-category h4 { - color: var(--primary); - font-weight: 600; - margin-bottom: 1rem; - font-size: 1.125rem; -} - -.skill-category ul { - list-style: none; - padding: 0; -} - -.skill-category li { - padding: 0.5rem 0; - color: var(--text-secondary); - border-bottom: 1px solid var(--border-dark); - position: relative; - padding-left: 1.5rem; -} - -.skill-category li:before { - content: '▸'; - position: absolute; - left: 0; - color: var(--primary); - font-weight: bold; -} - -.skill-category li:last-child { - border-bottom: none; -} - -/* ===== ENHANCED COMPETENCE STYLES ===== */ -.competence-grid { - display: grid; - gap: 2rem; -} - -.competence-item { - display: flex; - gap: 2rem; - align-items: flex-start; - padding: 2rem; - background: var(--bg-card); - border-radius: var(--radius-xl); - box-shadow: var(--shadow-lg); - border-left: 4px solid var(--secondary); - transition: var(--transition); - border: 1px solid var(--border-light); -} - -.competence-item:hover { - transform: translateX(8px); - box-shadow: var(--shadow-xl); - border-color: var(--primary-light); -} - -.competence-scan-wrapper { - flex-shrink: 0; -} - -.competence-scan-container { - width: 220px; - cursor: pointer; - border-radius: var(--radius-lg); - overflow: hidden; - border: 2px solid var(--border-light); - transition: var(--transition); - position: relative; - box-shadow: var(--shadow-md); -} - -.competence-scan-container:hover { - transform: translateY(-4px) scale(1.02); - box-shadow: var(--shadow-xl); - border-color: var(--primary); -} - -.competence-scan { - width: 100%; - height: auto; - display: block; - transition: var(--transition); -} - -.scan-hint { - position: absolute; - bottom: 0; - left: 0; - right: 0; - background: linear-gradient(transparent, rgba(0,0,0,0.8)); - color: white; - padding: 1rem; - text-align: center; - opacity: 0; - transition: var(--transition); - transform: translateY(10px); -} - -.competence-scan-container:hover .scan-hint { - opacity: 1; - transform: translateY(0); -} - -/* ===== ENHANCED RECALL STYLES ===== */ -.recall-grid { - display: grid; - gap: 2rem; -} - -.recall-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - margin-bottom: 1.5rem; - padding-bottom: 1.5rem; - border-bottom: 2px solid var(--border-light); -} - -.recall-title { - font-size: 1.375rem; - font-weight: 700; - color: var(--text-primary); - margin-bottom: 0.75rem; -} - -.recall-meta { - display: flex; - gap: 1rem; - font-size: 0.875rem; - font-weight: 500; -} - -.recall-date { - background: var(--gradient-primary); - color: white; - padding: 0.5rem 1rem; - border-radius: var(--radius-md); - font-weight: 600; -} - -.recall-client { - background: var(--gradient-secondary); - color: white; - padding: 0.5rem 1rem; - border-radius: var(--radius-md); - font-weight: 600; -} - -/* ===== ENHANCED PAGE HEADERS ===== */ -.page-header { - margin-bottom: 3rem; - padding: 3rem 0; - text-align: center; - background: linear-gradient(135deg, var(--bg-card) 0%, var(--bg-primary) 100%); - border-radius: var(--radius-xl); - box-shadow: var(--shadow-lg); - border: 1px solid var(--border-light); - position: relative; - overflow: hidden; -} - -.page-header::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 4px; - background: var(--gradient-primary); -} - -.page-title { - font-size: 3rem; - font-weight: 800; - color: var(--text-primary); - margin-bottom: 1rem; - background: var(--gradient-hero); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; - text-shadow: 0 4px 20px rgba(255, 107, 0, 0.3); -} - -.page-subtitle { - color: var(--text-secondary); - font-size: 1.25rem; - font-weight: 500; - max-width: 600px; - margin: 0 auto; -} - -/* ===== ENHANCED FOOTER ===== */ -.footer { - background: linear-gradient(135deg, var(--bg-tertiary) 0%, var(--bg-secondary) 100%); - color: var(--text-primary); - padding: 4rem 0 2rem; - margin-top: 6rem; - position: relative; - border-top: 1px solid var(--border-dark); -} - -.footer::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 4px; - background: var(--gradient-primary); -} - -.footer-content { - display: grid; - grid-template-columns: 1fr 1fr 1fr; - gap: 3rem; - margin-bottom: 3rem; -} - -.footer-info h3 { - margin-bottom: 1rem; - color: var(--text-primary); - font-size: 1.5rem; - font-weight: 700; -} - -.footer-info p { - opacity: 0.9; - font-size: 1.05rem; - color: var(--text-secondary); -} - -.footer-contacts p { - margin-bottom: 0.75rem; - opacity: 0.9; - display: flex; - align-items: center; - gap: 0.5rem; - font-size: 1.05rem; - color: var(--text-secondary); -} - -.footer-copyright { - grid-column: 1 / -1; - text-align: center; - padding-top: 2rem; - border-top: 1px solid var(--border-light); - opacity: 0.7; - font-size: 0.95rem; - color: var(--text-light); -} - -/* ===== ANIMATIONS ===== */ -@keyframes fadeInUp { - from { - opacity: 0; - transform: translateY(40px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -@keyframes float { - 0%, 100% { - transform: translateY(0); - } - 50% { - transform: translateY(-10px); - } -} - -@keyframes gradientShift { - 0% { - background-position: 0% 50%; - } - 50% { - background-position: 100% 50%; - } - 100% { - background-position: 0% 50%; - } -} - -.fade-in { - animation: fadeInUp 0.8s ease-out; -} - -.float { - animation: float 3s ease-in-out infinite; -} - -.gradient-animate { - background-size: 200% 200%; - animation: gradientShift 3s ease infinite; -} - -/* ===== RESPONSIVE DESIGN ===== */ -@media (max-width: 1024px) { - .nav-menu { - gap: 1.5rem; - } - - .nav-actions { - gap: 0.75rem; - } -} - -@media (max-width: 768px) { - .nav-menu { - display: none !important; - } - - .mobile-menu-btn { - display: block !important; - } - - .nav-actions .telegram-btn span:not(.telegram-icon), - .nav-actions .telegram-btn .telegram-icon { - display: none; - } - - .nav-actions .telegram-btn { - padding: 0.75rem; - width: 45px; - height: 45px; - display: flex; - align-items: center; - justify-content: center; - } - - .nav-actions .telegram-btn::after { - content: "📱"; - font-size: 1.2rem; - } - - .theme-switcher { - display: none; - } - - .nav { - gap: 1rem; - } - - .logo-text { - font-size: 1.1rem; - } -} - -@media (max-width: 480px) { - .container { - padding: 0 12px; - } - - .nav { - padding: 0.75rem 0; - } - - .logo-img { - width: 32px; - height: 32px; - } - - .logo-text { - font-size: 1rem; - } - - .mobile-menu { - width: 280px; - } - - .nav-actions { - gap: 0.5rem; - } - - .nav-actions .telegram-btn { - width: 40px; - height: 40px; - padding: 0.5rem; - } -} - -@media (min-width: 769px) { - .mobile-menu-btn, - .mobile-menu-overlay, - .mobile-menu { - display: none !important; - } -} - -/* ===== UTILITY CLASSES ===== */ -.text-center { text-align: center; } -.text-left { text-align: left; } -.text-right { text-align: right; } - -.justify-center { justify-content: center; } -.justify-between { justify-content: space-between; } - -.mb-1 { margin-bottom: 0.5rem; } -.mb-2 { margin-bottom: 1rem; } -.mb-3 { margin-bottom: 1.5rem; } -.mb-4 { margin-bottom: 2rem; } - -.mt-1 { margin-top: 0.5rem; } -.mt-2 { margin-top: 1rem; } -.mt-3 { margin-top: 1.5rem; } -.mt-4 { margin-top: 2rem; } - -.hidden { display: none; } - -/* ===== CUSTOM COMPONENTS ===== */ -.content-card { - background: var(--bg-card); - border-radius: var(--radius-xl); - padding: 2.5rem; - box-shadow: var(--shadow-lg); - border: 1px solid var(--border-light); - margin-bottom: 2rem; - position: relative; - overflow: hidden; -} - -.content-card::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 4px; - background: var(--gradient-primary); -} - -.improved-list { - list-style: none; - padding: 0; -} - -.improved-list li { - margin-bottom: 2rem; -} - -/* ===== FORM STYLES ===== */ -.form-group { - margin-bottom: 1.5rem; -} - -.form-group label { - display: block; - margin-bottom: 0.5rem; - font-weight: 600; - color: var(--text-primary); -} - -.form-input, .form-textarea { - width: 100%; - padding: 1rem 1.25rem; - border: 2px solid var(--border-light); - border-radius: var(--radius-lg); - font-size: 1rem; - transition: var(--transition); - background: var(--bg-primary); - color: var(--text-primary); - font-family: inherit; -} - -.form-input::placeholder, .form-textarea::placeholder { - color: var(--text-light); -} - -.form-input:focus, .form-textarea:focus { - outline: none; - border-color: var(--primary); - box-shadow: 0 0 0 4px rgba(255, 107, 0, 0.2); - transform: translateY(-2px); - background: var(--bg-card); -} - -.form-textarea { - resize: vertical; - min-height: 120px; - line-height: 1.5; -} - -/* ===== MODAL ENHANCEMENTS ===== */ -.modal { - display: none; - position: fixed; - z-index: 1000; - left: 0; - top: 0; - width: 100%; - height: 100%; - background-color: rgba(15, 19, 31, 0.95); - backdrop-filter: blur(10px); - animation: fadeIn 0.3s ease; -} - -.modal-content { - background: var(--bg-card); - margin: 2% auto; - border-radius: var(--radius-xl); - box-shadow: var(--shadow-xl); - animation: slideIn 0.3s ease; - position: relative; - max-width: 90%; - max-height: 90vh; - overflow: auto; - border: 1px solid var(--border-light); -} - -.modal-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 2rem 2rem 1rem; - border-bottom: 2px solid var(--border-light); -} - -.modal-header h3 { - margin: 0; - color: var(--text-primary); - font-size: 1.5rem; - font-weight: 700; - background: var(--gradient-primary); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.modal-body { - padding: 2rem; -} - -.modal-close { - color: var(--text-light); - font-size: 2rem; - font-weight: bold; - cursor: pointer; - transition: var(--transition); - background: none; - border: none; - padding: 0; - width: 40px; - height: 40px; - display: flex; - align-items: center; - justify-content: center; - border-radius: var(--radius-md); -} - -.modal-close:hover { - color: var(--primary); - background: var(--bg-primary); - transform: rotate(90deg); -} - -@keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } -} - -@keyframes slideIn { - from { - opacity: 0; - transform: translateY(-50px) scale(0.9); - } - to { - opacity: 1; - transform: translateY(0) scale(1); - } -} - -/* ===== SOLUTION PAGE STYLES ===== */ -.solution-accordion { - margin: 2rem 0; -} - -.accordion-item { - background: var(--bg-primary); - border-radius: var(--radius-lg); - margin-bottom: 1rem; - border: 2px solid var(--border-light); - overflow: hidden; - transition: var(--transition); -} - -.accordion-item:hover { - border-color: var(--primary-light); - transform: translateX(4px); -} - -.accordion-header { - padding: 1.5rem; - background: var(--gradient-primary); - color: white; - font-weight: 600; - font-size: 1.125rem; - cursor: pointer; - display: flex; - justify-content: space-between; - align-items: center; - transition: var(--transition); -} - -.accordion-header:hover { - background: var(--primary-dark); -} - -.accordion-content { - padding: 1.5rem; - background: var(--bg-card); - line-height: 1.7; - color: var(--text-secondary); -} - -.accordion-content p { - margin-bottom: 1rem; -} - -.accordion-content p:last-child { - margin-bottom: 0; -} - -/* ===== MAIN CONTENT AREA ===== */ -.main { - min-height: calc(100vh - 140px); - padding: 2rem 0; -} - -.content { - min-height: 600px; -} - -.breadcrumbs { - display: flex; - align-items: center; - gap: 0.5rem; - margin-bottom: 2rem; - font-size: 0.875rem; - color: var(--text-light); -} - -.breadcrumb-link { - color: var(--text-secondary); - text-decoration: none; - transition: var(--transition); -} - -.breadcrumb-link:hover { - color: var(--primary); -} - -.breadcrumb-separator { - color: var(--text-light); -} - -.breadcrumb-current { - color: var(--text-primary); - font-weight: 500; -} - -.page-content { - background: var(--bg-card); - border-radius: var(--radius-xl); - padding: 2rem; - box-shadow: var(--shadow-lg); - border: 1px solid var(--border-light); -} - - -/* ===== THEME SWITCHER STYLES ===== */ -.theme-switcher { - display: flex; - align-items: center; -} - -.theme-toggle-checkbox { - display: none; -} - -.theme-toggle-label { - position: relative; - display: flex; - align-items: center; - width: 60px; - height: 30px; - background: var(--bg-card); - border: 2px solid var(--border-light); - border-radius: 25px; - cursor: pointer; - transition: all 0.3s ease; - padding: 2px; - box-shadow: var(--shadow-sm); -} - -.theme-toggle-label:hover { - border-color: var(--primary); - box-shadow: 0 0 10px rgba(255, 107, 0, 0.3); -} - -.theme-toggle-slider { - position: absolute; - width: 24px; - height: 24px; - background: var(--primary); - border-radius: 50%; - transition: all 0.3s ease; - left: 2px; - z-index: 2; -} - -.theme-toggle-checkbox:checked + .theme-toggle-label .theme-toggle-slider { - transform: translateX(30px); - background: var(--secondary); -} - -.theme-icon { - position: absolute; - font-size: 12px; - transition: all 0.3s ease; - z-index: 1; -} - -.theme-icon.sun { - left: 8px; - opacity: 1; -} - -.theme-icon.moon { - right: 8px; - opacity: 0; -} - -.theme-toggle-checkbox:checked + .theme-toggle-label .theme-icon.sun { - opacity: 0; -} - -.theme-toggle-checkbox:checked + .theme-toggle-label .theme-icon.moon { - opacity: 1; -} - -/* ===== MOBILE RESPONSIVE STYLES ===== */ -@media (max-width: 768px) { - .hero-title { - font-size: 2.5rem; - } - - .hero-subtitle { - font-size: 1.125rem; - padding: 0 1rem; - } - - .page-title { - font-size: 2.25rem; - } - - .page-subtitle { - font-size: 1.125rem; - padding: 0 1rem; - } - - .grid-2 { - grid-template-columns: 1fr; - gap: 1.5rem; - } - - .modern-card { - padding: 1.5rem; - margin-bottom: 1.5rem; - } - - .card-title { - font-size: 1.375rem; - } - - .content-card { - padding: 1.5rem; - } - - .about-section { - padding: 1.5rem; - } - - .skills-grid { - grid-template-columns: 1fr; - gap: 1.5rem; - } - - .competence-item { - flex-direction: column; - padding: 1.5rem; - gap: 1.5rem; - } - - .competence-scan-container { - width: 100%; - max-width: 280px; - margin: 0 auto; - } - - .recall-header { - flex-direction: column; - gap: 1rem; - align-items: flex-start; - } - - .recall-meta { - flex-direction: column; - gap: 0.5rem; - } - - .footer-content { - grid-template-columns: 1fr; - gap: 2rem; - text-align: center; - } - - .footer-contacts p { - justify-content: center; - } - - .card-actions { - flex-direction: column; - } - - .btn { - width: 100%; - justify-content: center; - } - - .modal-content { - margin: 5% auto; - max-width: 95%; - } - - .modal-header { - padding: 1.5rem 1.5rem 1rem; - } - - .modal-body { - padding: 1.5rem; - } - - .page-content { - padding: 1.5rem; - } - - .breadcrumbs { - font-size: 0.8rem; - } -} - -@media (max-width: 480px) { - .hero-title { - font-size: 2rem; - } - - .hero-subtitle { - font-size: 1rem; - } - - .page-title { - font-size: 1.875rem; - } - - .modern-card { - padding: 1.25rem; - } - - .content-card { - padding: 1.25rem; - } - - .about-section { - padding: 1.25rem; - } - - .card-title { - font-size: 1.25rem; - } - - .competence-scan-container { - max-width: 100%; - } -} - -/* Стили для страницы отзывов */ -.recall-item { - display: flex; - flex-direction: column; - gap: 1.5rem; -} - -.recall-content { - display: flex; - gap: 2rem; - align-items: flex-start; -} - -.recall-scan-wrapper { - flex-shrink: 0; -} - -.recall-scan-container { - width: 280px; - cursor: pointer; - border-radius: var(--radius-lg); - overflow: hidden; - border: 2px solid var(--border-light); - transition: var(--transition); - position: relative; - box-shadow: var(--shadow-md); -} - -.recall-scan-container:hover { - transform: translateY(-4px) scale(1.02); - box-shadow: var(--shadow-xl); - border-color: var(--primary); -} - -.recall-scan { - width: 100%; - height: auto; - display: block; - transition: var(--transition); -} - -.recall-text { - flex: 1; - min-width: 0; - line-height: 1.7; - font-size: 1.05rem; -} - -.scan-hint { - position: absolute; - bottom: 0; - left: 0; - right: 0; - background: linear-gradient(transparent, rgba(0,0,0,0.8)); - color: white; - padding: 1rem; - text-align: center; - opacity: 0; - transition: var(--transition); - transform: translateY(10px); -} - -.recall-scan-container:hover .scan-hint { - opacity: 1; - transform: translateY(0); -} - -/* Стили для модального окна с изображением */ -.modal-image { - max-width: 90vw; - max-height: 80vh; - width: auto; - height: auto; - display: block; - margin: 0 auto; - border-radius: var(--radius-md); - box-shadow: var(--shadow-xl); - transition: all 0.3s ease; -} - -.modal-content.image-modal { - max-width: 95vw; - max-height: 95vh; - background: transparent; - border: none; - box-shadow: none; -} - -.modal-body.image-modal-body { - padding: 1rem; - display: flex; - align-items: center; - justify-content: center; - background: transparent; -} - -/* Адаптивность для мобильных устройств */ -@media (max-width: 768px) { - .recall-content { - flex-direction: column; - gap: 1.5rem; - } - - .recall-scan-container { - width: 100%; - max-width: 300px; - margin: 0 auto; - } - - .recall-scan-wrapper { - order: -1; - } - - .modal-image { - max-width: 95vw; - max-height: 70vh; - } - - .modal-content.image-modal { - margin: 10% auto; - } -} - -@media (max-width: 480px) { - .recall-scan-container { - max-width: 100%; - } - - .modal-content.image-modal { - margin: 5% auto; - max-width: 98vw; - } - - .modal-body.image-modal-body { - padding: 0.5rem; - } +/* ===== CSS RESET & VARIABLES ===== */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + /* 1C Brand Colors - Modern Gradient Palette */ + --primary: #FF6B00; /* 1C Orange */ + --primary-dark: #E55A00; + --primary-light: #FF8A3D; + --secondary: #0055A5; /* 1C Blue */ + --secondary-dark: #004488; + --secondary-light: #3377CC; + --accent: #00A8FF; + + /* Extended 1C Color Palette */ + --gradient-primary: linear-gradient(135deg, #FF6B00 0%, #FF8A3D 100%); + --gradient-secondary: linear-gradient(135deg, #0055A5 0%, #3377CC 100%); + --gradient-hero: linear-gradient(135deg, #FF6B00 0%, #0055A5 100%); + + /* Dark Theme Colors */ + --bg-primary: #1A1F2E; + --bg-secondary: #151925; + --bg-tertiary: #0F131F; + --bg-card: #222738; + + --text-primary: #FFFFFF; + --text-secondary: #B0B8D1; + --text-light: #8A93B0; + + --border-light: #2D3447; + --border-medium: #3A4158; + --border-dark: #252A3A; + + /* Modern Shadows */ + --shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.3), 0 1px 2px 0 rgba(0, 0, 0, 0.2); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -1px rgba(0, 0, 0, 0.3); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.5), 0 4px 6px -2px rgba(0, 0, 0, 0.4); + --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.6), 0 10px 10px -5px rgba(0, 0, 0, 0.5); + + /* Border radius */ + --radius-sm: 8px; + --radius-md: 12px; + --radius-lg: 16px; + --radius-xl: 20px; + + /* Transitions */ + --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + --transition-slow: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); +} + +/* ===== BASE STYLES ===== */ +html { + scroll-behavior: smooth; +} + +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + line-height: 1.6; + color: var(--text-primary); + background: linear-gradient(135deg, var(--bg-tertiary) 0%, var(--bg-secondary) 50%, var(--bg-primary) 100%); + font-size: 16px; + overflow-x: hidden; + min-height: 100vh; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 20px; +} + +/* ===== ENHANCED HEADER ===== */ +.header { + background: rgba(26, 31, 46, 0.95); + border-bottom: 1px solid var(--border-dark); + box-shadow: var(--shadow-lg); + position: sticky; + top: 0; + z-index: 1000; + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); +} + +.nav { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem 0; + gap: 2rem; +} + +.logo { + display: flex; + align-items: center; + text-decoration: none; + font-weight: 700; + font-size: 1.375rem; + color: var(--text-primary); + transition: var(--transition); + background: var(--gradient-primary); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.logo:hover { + transform: translateY(-1px); +} + +.logo-img { + width: 40px; + height: 40px; + margin-right: 12px; + border-radius: var(--radius-md); + background: var(--gradient-primary); + padding: 4px; + filter: drop-shadow(0 4px 8px rgba(255, 107, 0, 0.3)); +} + +.nav-menu { + display: flex; + list-style: none; + gap: 2.5rem; + margin: 0; + flex-wrap: wrap; +} + +.nav-link { + text-decoration: none; + color: var(--text-secondary); + font-weight: 600; + padding: 0.75rem 0; + position: relative; + transition: var(--transition); + font-size: 1rem; +} + +.nav-link:hover { + color: var(--primary); +} + +.nav-link.active { + color: var(--primary); + font-weight: 700; +} + +.nav-link.active::after { + content: ''; + position: absolute; + bottom: -2px; + left: 0; + right: 0; + height: 3px; + background: var(--gradient-primary); + border-radius: 2px; +} + +.telegram-btn { + display: flex; + align-items: center; + gap: 10px; + background: var(--gradient-primary); + color: white; + text-decoration: none; + padding: 0.6rem 1.2rem; + border-radius: var(--radius-lg); + font-weight: 600; + transition: var(--transition); + white-space: nowrap; + box-shadow: var(--shadow-md); + border: none; + cursor: pointer; + font-size: 0.95rem; +} + +.telegram-btn:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-lg); + background: var(--gradient-primary); +} + +/* ===== ENHANCED HERO SECTION ===== */ +.hero-section { + text-align: center; + padding: 5rem 0 4rem; + margin-bottom: 3rem; + position: relative; +} + +.hero-section::before { + content: ''; + position: absolute; + top: 0; + left: 50%; + transform: translateX(-50%); + width: 200px; + height: 200px; + background: radial-gradient(circle, rgba(255, 107, 0, 0.2) 0%, transparent 70%); + border-radius: 50%; +} + +.hero-title { + font-size: 3.5rem; + font-weight: 800; + margin-bottom: 1.5rem; + line-height: 1.1; + background: var(--gradient-hero); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-shadow: 0 4px 20px rgba(255, 107, 0, 0.3); +} + +.hero-subtitle { + font-size: 1.375rem; + color: var(--text-secondary); + font-weight: 500; + max-width: 600px; + margin: 0 auto; + line-height: 1.6; +} + +/* ===== MODERN CARD STYLES ===== */ +.modern-card { + background: var(--bg-card); + border-radius: var(--radius-xl); + padding: 2rem; + box-shadow: var(--shadow-lg); + border: 1px solid var(--border-light); + transition: var(--transition-slow); + margin-bottom: 2rem; + position: relative; + overflow: hidden; +} + +.modern-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: var(--gradient-primary); +} + +.modern-card:hover { + transform: translateY(-8px); + box-shadow: var(--shadow-xl); + border-color: var(--primary-light); +} + +.modern-card.secondary::before { + background: var(--gradient-secondary); +} + +.card-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + margin-bottom: 1.5rem; + gap: 1rem; +} + +.card-title { + font-size: 1.5rem; + font-weight: 700; + color: var(--text-primary); + margin-bottom: 0.75rem; + background: var(--gradient-primary); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.card-subtitle { + color: var(--text-secondary); + font-size: 1rem; + margin-bottom: 1.5rem; + line-height: 1.6; +} + +.card-content { + color: var(--text-primary); + line-height: 1.7; + font-size: 1.05rem; +} + +.card-content p { + margin-bottom: 1rem; +} + +.card-actions { + margin-top: 2rem; + display: flex; + gap: 1rem; + flex-wrap: wrap; +} + +/* ===== GRID LAYOUTS ===== */ +.grid { + display: grid; + gap: 2rem; +} + +.grid-2 { + grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); +} + +.grid-3 { + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); +} + +/* ===== ENHANCED BUTTONS ===== */ +.btn { + display: inline-flex; + align-items: center; + gap: 0.75rem; + padding: 1rem 2rem; + border-radius: var(--radius-lg); + text-decoration: none; + font-weight: 600; + border: none; + cursor: pointer; + transition: var(--transition); + font-size: 1rem; + position: relative; + overflow: hidden; +} + +.btn::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent); + transition: left 0.5s; +} + +.btn:hover::before { + left: 100%; +} + +.btn-primary { + background: var(--gradient-primary); + color: white; + box-shadow: var(--shadow-md); +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-lg); +} + +.btn-secondary { + background: var(--gradient-secondary); + color: white; + box-shadow: var(--shadow-md); +} + +.btn-secondary:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-lg); +} + +.btn-outline { + background: transparent; + color: var(--primary); + border: 2px solid var(--primary); +} + +.btn-outline:hover { + background: var(--primary); + color: white; + transform: translateY(-2px); +} + +/* ===== ENHANCED ABOUT PAGE ===== */ +.about-header { + text-align: center; + margin-bottom: 3rem; + padding: 3rem 0; + background: linear-gradient(135deg, var(--bg-card) 0%, var(--bg-primary) 100%); + border-radius: var(--radius-xl); + box-shadow: var(--shadow-lg); + border: 1px solid var(--border-light); +} + +.about-header h2 { + font-size: 2.5rem; + font-weight: 800; + margin-bottom: 0.5rem; + background: var(--gradient-hero); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.about-header .subtitle { + font-size: 1.25rem; + color: var(--text-secondary); + font-weight: 500; +} + +.about-section { + margin-bottom: 3rem; + padding: 2.5rem; + background: var(--bg-card); + border-radius: var(--radius-xl); + box-shadow: var(--shadow-lg); + border-left: 4px solid var(--primary); + border: 1px solid var(--border-light); +} + +.about-section h3 { + font-size: 1.5rem; + font-weight: 700; + margin-bottom: 1.5rem; + color: var(--text-primary); + display: flex; + align-items: center; + gap: 0.75rem; +} + +.skills-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 2rem; + margin-top: 1.5rem; +} + +.skill-category { + background: var(--bg-primary); + padding: 1.5rem; + border-radius: var(--radius-lg); + border: 1px solid var(--border-light); + transition: var(--transition); +} + +.skill-category:hover { + transform: translateY(-4px); + box-shadow: var(--shadow-md); + border-color: var(--primary-light); +} + +.skill-category h4 { + color: var(--primary); + font-weight: 600; + margin-bottom: 1rem; + font-size: 1.125rem; +} + +.skill-category ul { + list-style: none; + padding: 0; +} + +.skill-category li { + padding: 0.5rem 0; + color: var(--text-secondary); + border-bottom: 1px solid var(--border-dark); + position: relative; + padding-left: 1.5rem; +} + +.skill-category li:before { + content: '▸'; + position: absolute; + left: 0; + color: var(--primary); + font-weight: bold; +} + +.skill-category li:last-child { + border-bottom: none; +} + +/* ===== ENHANCED COMPETENCE STYLES ===== */ +.competence-grid { + display: grid; + gap: 2rem; +} + +.competence-item { + display: flex; + gap: 2rem; + align-items: flex-start; + padding: 2rem; + background: var(--bg-card); + border-radius: var(--radius-xl); + box-shadow: var(--shadow-lg); + border-left: 4px solid var(--secondary); + transition: var(--transition); + border: 1px solid var(--border-light); +} + +.competence-item:hover { + transform: translateX(8px); + box-shadow: var(--shadow-xl); + border-color: var(--primary-light); +} + +.competence-scan-wrapper { + flex-shrink: 0; +} + +.competence-scan-container { + width: 220px; + cursor: pointer; + border-radius: var(--radius-lg); + overflow: hidden; + border: 2px solid var(--border-light); + transition: var(--transition); + position: relative; + box-shadow: var(--shadow-md); +} + +.competence-scan-container:hover { + transform: translateY(-4px) scale(1.02); + box-shadow: var(--shadow-xl); + border-color: var(--primary); +} + +.competence-scan { + width: 100%; + height: auto; + display: block; + transition: var(--transition); +} + +.scan-hint { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: linear-gradient(transparent, rgba(0,0,0,0.8)); + color: white; + padding: 1rem; + text-align: center; + opacity: 0; + transition: var(--transition); + transform: translateY(10px); +} + +.competence-scan-container:hover .scan-hint { + opacity: 1; + transform: translateY(0); +} + +/* ===== ENHANCED RECALL STYLES ===== */ +.recall-grid { + display: grid; + gap: 2rem; +} + +.recall-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 1.5rem; + padding-bottom: 1.5rem; + border-bottom: 2px solid var(--border-light); +} + +.recall-title { + font-size: 1.375rem; + font-weight: 700; + color: var(--text-primary); + margin-bottom: 0.75rem; +} + +.recall-meta { + display: flex; + gap: 1rem; + font-size: 0.875rem; + font-weight: 500; +} + +.recall-date { + background: var(--gradient-primary); + color: white; + padding: 0.5rem 1rem; + border-radius: var(--radius-md); + font-weight: 600; +} + +.recall-client { + background: var(--gradient-secondary); + color: white; + padding: 0.5rem 1rem; + border-radius: var(--radius-md); + font-weight: 600; +} + +/* ===== ENHANCED PAGE HEADERS ===== */ +.page-header { + margin-bottom: 3rem; + padding: 3rem 0; + text-align: center; + background: linear-gradient(135deg, var(--bg-card) 0%, var(--bg-primary) 100%); + border-radius: var(--radius-xl); + box-shadow: var(--shadow-lg); + border: 1px solid var(--border-light); + position: relative; + overflow: hidden; +} + +.page-header::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: var(--gradient-primary); +} + +.page-title { + font-size: 3rem; + font-weight: 800; + color: var(--text-primary); + margin-bottom: 1rem; + background: var(--gradient-hero); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-shadow: 0 4px 20px rgba(255, 107, 0, 0.3); +} + +.page-subtitle { + color: var(--text-secondary); + font-size: 1.25rem; + font-weight: 500; + max-width: 600px; + margin: 0 auto; +} + +/* ===== ENHANCED FOOTER ===== */ +.footer { + background: linear-gradient(135deg, var(--bg-tertiary) 0%, var(--bg-secondary) 100%); + color: var(--text-primary); + padding: 4rem 0 2rem; + margin-top: 6rem; + position: relative; + border-top: 1px solid var(--border-dark); +} + +.footer::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: var(--gradient-primary); +} + +.footer-content { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 3rem; + margin-bottom: 3rem; +} + +.footer-info h3 { + margin-bottom: 1rem; + color: var(--text-primary); + font-size: 1.5rem; + font-weight: 700; +} + +.footer-info p { + opacity: 0.9; + font-size: 1.05rem; + color: var(--text-secondary); +} + +.footer-contacts p { + margin-bottom: 0.75rem; + opacity: 0.9; + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 1.05rem; + color: var(--text-secondary); +} + +.footer-copyright { + grid-column: 1 / -1; + text-align: center; + padding-top: 2rem; + border-top: 1px solid var(--border-light); + opacity: 0.7; + font-size: 0.95rem; + color: var(--text-light); +} + +/* ===== ANIMATIONS ===== */ +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(40px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes float { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-10px); + } +} + +@keyframes gradientShift { + 0% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } + 100% { + background-position: 0% 50%; + } +} + +.fade-in { + animation: fadeInUp 0.8s ease-out; +} + +.float { + animation: float 3s ease-in-out infinite; +} + +.gradient-animate { + background-size: 200% 200%; + animation: gradientShift 3s ease infinite; +} + +/* ===== RESPONSIVE DESIGN ===== */ +@media (max-width: 1024px) { + .nav-menu { + gap: 1.5rem; + } + + .nav-actions { + gap: 0.75rem; + } +} + +@media (max-width: 768px) { + .nav-menu { + display: none !important; + } + + .mobile-menu-btn { + display: block !important; + } + + .nav-actions .telegram-btn span:not(.telegram-icon), + .nav-actions .telegram-btn .telegram-icon { + display: none; + } + + .nav-actions .telegram-btn { + padding: 0.75rem; + width: 45px; + height: 45px; + display: flex; + align-items: center; + justify-content: center; + } + + .nav-actions .telegram-btn::after { + content: "📱"; + font-size: 1.2rem; + } + + .theme-switcher { + display: none; + } + + .nav { + gap: 1rem; + } + + .logo-text { + font-size: 1.1rem; + } +} + +@media (max-width: 480px) { + .container { + padding: 0 12px; + } + + .nav { + padding: 0.75rem 0; + } + + .logo-img { + width: 32px; + height: 32px; + } + + .logo-text { + font-size: 1rem; + } + + .mobile-menu { + width: 280px; + } + + .nav-actions { + gap: 0.5rem; + } + + .nav-actions .telegram-btn { + width: 40px; + height: 40px; + padding: 0.5rem; + } +} + +@media (min-width: 769px) { + .mobile-menu-btn, + .mobile-menu-overlay, + .mobile-menu { + display: none !important; + } +} + +/* ===== UTILITY CLASSES ===== */ +.text-center { text-align: center; } +.text-left { text-align: left; } +.text-right { text-align: right; } + +.justify-center { justify-content: center; } +.justify-between { justify-content: space-between; } + +.mb-1 { margin-bottom: 0.5rem; } +.mb-2 { margin-bottom: 1rem; } +.mb-3 { margin-bottom: 1.5rem; } +.mb-4 { margin-bottom: 2rem; } + +.mt-1 { margin-top: 0.5rem; } +.mt-2 { margin-top: 1rem; } +.mt-3 { margin-top: 1.5rem; } +.mt-4 { margin-top: 2rem; } + +.hidden { display: none; } + +/* ===== CUSTOM COMPONENTS ===== */ +.content-card { + background: var(--bg-card); + border-radius: var(--radius-xl); + padding: 2.5rem; + box-shadow: var(--shadow-lg); + border: 1px solid var(--border-light); + margin-bottom: 2rem; + position: relative; + overflow: hidden; +} + +.content-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: var(--gradient-primary); +} + +.improved-list { + list-style: none; + padding: 0; +} + +.improved-list li { + margin-bottom: 2rem; +} + +/* ===== FORM STYLES ===== */ +.form-group { + margin-bottom: 1.5rem; +} + +.form-group label { + display: block; + margin-bottom: 0.5rem; + font-weight: 600; + color: var(--text-primary); +} + +.form-input, .form-textarea { + width: 100%; + padding: 1rem 1.25rem; + border: 2px solid var(--border-light); + border-radius: var(--radius-lg); + font-size: 1rem; + transition: var(--transition); + background: var(--bg-primary); + color: var(--text-primary); + font-family: inherit; +} + +.form-input::placeholder, .form-textarea::placeholder { + color: var(--text-light); +} + +.form-input:focus, .form-textarea:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 4px rgba(255, 107, 0, 0.2); + transform: translateY(-2px); + background: var(--bg-card); +} + +.form-textarea { + resize: vertical; + min-height: 120px; + line-height: 1.5; +} + +/* ===== MODAL ENHANCEMENTS ===== */ +.modal { + display: none; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(15, 19, 31, 0.95); + backdrop-filter: blur(10px); + animation: fadeIn 0.3s ease; +} + +.modal-content { + background: var(--bg-card); + margin: 2% auto; + border-radius: var(--radius-xl); + box-shadow: var(--shadow-xl); + animation: slideIn 0.3s ease; + position: relative; + max-width: 90%; + max-height: 90vh; + overflow: auto; + border: 1px solid var(--border-light); +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 2rem 2rem 1rem; + border-bottom: 2px solid var(--border-light); +} + +.modal-header h3 { + margin: 0; + color: var(--text-primary); + font-size: 1.5rem; + font-weight: 700; + background: var(--gradient-primary); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.modal-body { + padding: 2rem; +} + +.modal-close { + color: var(--text-light); + font-size: 2rem; + font-weight: bold; + cursor: pointer; + transition: var(--transition); + background: none; + border: none; + padding: 0; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-md); +} + +.modal-close:hover { + color: var(--primary); + background: var(--bg-primary); + transform: rotate(90deg); +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(-50px) scale(0.9); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +/* ===== SOLUTION PAGE STYLES ===== */ +.solution-accordion { + margin: 2rem 0; +} + +.accordion-item { + background: var(--bg-primary); + border-radius: var(--radius-lg); + margin-bottom: 1rem; + border: 2px solid var(--border-light); + overflow: hidden; + transition: var(--transition); +} + +.accordion-item:hover { + border-color: var(--primary-light); + transform: translateX(4px); +} + +.accordion-header { + padding: 1.5rem; + background: var(--gradient-primary); + color: white; + font-weight: 600; + font-size: 1.125rem; + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + transition: var(--transition); +} + +.accordion-header:hover { + background: var(--primary-dark); +} + +.accordion-content { + padding: 1.5rem; + background: var(--bg-card); + line-height: 1.7; + color: var(--text-secondary); +} + +.accordion-content p { + margin-bottom: 1rem; +} + +.accordion-content p:last-child { + margin-bottom: 0; +} + +/* ===== MAIN CONTENT AREA ===== */ +.main { + min-height: calc(100vh - 140px); + padding: 2rem 0; +} + +.content { + min-height: 600px; +} + +.breadcrumbs { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 2rem; + font-size: 0.875rem; + color: var(--text-light); +} + +.breadcrumb-link { + color: var(--text-secondary); + text-decoration: none; + transition: var(--transition); +} + +.breadcrumb-link:hover { + color: var(--primary); +} + +.breadcrumb-separator { + color: var(--text-light); +} + +.breadcrumb-current { + color: var(--text-primary); + font-weight: 500; +} + +.page-content { + background: var(--bg-card); + border-radius: var(--radius-xl); + padding: 2rem; + box-shadow: var(--shadow-lg); + border: 1px solid var(--border-light); +} + + +/* ===== THEME SWITCHER STYLES ===== */ +.theme-switcher { + display: flex; + align-items: center; +} + +.theme-toggle-checkbox { + display: none; +} + +.theme-toggle-label { + position: relative; + display: flex; + align-items: center; + width: 60px; + height: 30px; + background: var(--bg-card); + border: 2px solid var(--border-light); + border-radius: 25px; + cursor: pointer; + transition: all 0.3s ease; + padding: 2px; + box-shadow: var(--shadow-sm); +} + +.theme-toggle-label:hover { + border-color: var(--primary); + box-shadow: 0 0 10px rgba(255, 107, 0, 0.3); +} + +.theme-toggle-slider { + position: absolute; + width: 24px; + height: 24px; + background: var(--primary); + border-radius: 50%; + transition: all 0.3s ease; + left: 2px; + z-index: 2; +} + +.theme-toggle-checkbox:checked + .theme-toggle-label .theme-toggle-slider { + transform: translateX(30px); + background: var(--secondary); +} + +.theme-icon { + position: absolute; + font-size: 12px; + transition: all 0.3s ease; + z-index: 1; +} + +.theme-icon.sun { + left: 8px; + opacity: 1; +} + +.theme-icon.moon { + right: 8px; + opacity: 0; +} + +.theme-toggle-checkbox:checked + .theme-toggle-label .theme-icon.sun { + opacity: 0; +} + +.theme-toggle-checkbox:checked + .theme-toggle-label .theme-icon.moon { + opacity: 1; +} + +/* ===== MOBILE RESPONSIVE STYLES ===== */ +@media (max-width: 768px) { + .hero-title { + font-size: 2.5rem; + } + + .hero-subtitle { + font-size: 1.125rem; + padding: 0 1rem; + } + + .page-title { + font-size: 2.25rem; + } + + .page-subtitle { + font-size: 1.125rem; + padding: 0 1rem; + } + + .grid-2 { + grid-template-columns: 1fr; + gap: 1.5rem; + } + + .modern-card { + padding: 1.5rem; + margin-bottom: 1.5rem; + } + + .card-title { + font-size: 1.375rem; + } + + .content-card { + padding: 1.5rem; + } + + .about-section { + padding: 1.5rem; + } + + .skills-grid { + grid-template-columns: 1fr; + gap: 1.5rem; + } + + .competence-item { + flex-direction: column; + padding: 1.5rem; + gap: 1.5rem; + } + + .competence-scan-container { + width: 100%; + max-width: 280px; + margin: 0 auto; + } + + .recall-header { + flex-direction: column; + gap: 1rem; + align-items: flex-start; + } + + .recall-meta { + flex-direction: column; + gap: 0.5rem; + } + + .footer-content { + grid-template-columns: 1fr; + gap: 2rem; + text-align: center; + } + + .footer-contacts p { + justify-content: center; + } + + .card-actions { + flex-direction: column; + } + + .btn { + width: 100%; + justify-content: center; + } + + .modal-content { + margin: 5% auto; + max-width: 95%; + } + + .modal-header { + padding: 1.5rem 1.5rem 1rem; + } + + .modal-body { + padding: 1.5rem; + } + + .page-content { + padding: 1.5rem; + } + + .breadcrumbs { + font-size: 0.8rem; + } +} + +@media (max-width: 480px) { + .hero-title { + font-size: 2rem; + } + + .hero-subtitle { + font-size: 1rem; + } + + .page-title { + font-size: 1.875rem; + } + + .modern-card { + padding: 1.25rem; + } + + .content-card { + padding: 1.25rem; + } + + .about-section { + padding: 1.25rem; + } + + .card-title { + font-size: 1.25rem; + } + + .competence-scan-container { + max-width: 100%; + } +} + +/* Стили для страницы отзывов */ +.recall-item { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.recall-content { + display: flex; + gap: 2rem; + align-items: flex-start; +} + +.recall-scan-wrapper { + flex-shrink: 0; +} + +.recall-scan-container { + width: 280px; + cursor: pointer; + border-radius: var(--radius-lg); + overflow: hidden; + border: 2px solid var(--border-light); + transition: var(--transition); + position: relative; + box-shadow: var(--shadow-md); +} + +.recall-scan-container:hover { + transform: translateY(-4px) scale(1.02); + box-shadow: var(--shadow-xl); + border-color: var(--primary); +} + +.recall-scan { + width: 100%; + height: auto; + display: block; + transition: var(--transition); +} + +.recall-text { + flex: 1; + min-width: 0; + line-height: 1.7; + font-size: 1.05rem; +} + +.scan-hint { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: linear-gradient(transparent, rgba(0,0,0,0.8)); + color: white; + padding: 1rem; + text-align: center; + opacity: 0; + transition: var(--transition); + transform: translateY(10px); +} + +.recall-scan-container:hover .scan-hint { + opacity: 1; + transform: translateY(0); +} + +/* Стили для модального окна с изображением */ +.modal-image { + max-width: 90vw; + max-height: 80vh; + width: auto; + height: auto; + display: block; + margin: 0 auto; + border-radius: var(--radius-md); + box-shadow: var(--shadow-xl); + transition: all 0.3s ease; +} + +.modal-content.image-modal { + max-width: 95vw; + max-height: 95vh; + background: transparent; + border: none; + box-shadow: none; +} + +.modal-body.image-modal-body { + padding: 1rem; + display: flex; + align-items: center; + justify-content: center; + background: transparent; +} + +/* Адаптивность для мобильных устройств */ +@media (max-width: 768px) { + .recall-content { + flex-direction: column; + gap: 1.5rem; + } + + .recall-scan-container { + width: 100%; + max-width: 300px; + margin: 0 auto; + } + + .recall-scan-wrapper { + order: -1; + } + + .modal-image { + max-width: 95vw; + max-height: 70vh; + } + + .modal-content.image-modal { + margin: 10% auto; + } +} + +@media (max-width: 480px) { + .recall-scan-container { + max-width: 100%; + } + + .modal-content.image-modal { + margin: 5% auto; + max-width: 98vw; + } + + .modal-body.image-modal-body { + padding: 0.5rem; + } } \ No newline at end of file diff --git a/OneCprogsite/programmer/static/programmer/css/styles_w.css b/OneCprogsite/programmer/static/programmer/css/styles_w.css index baa7f16..5dd6e1d 100644 --- a/OneCprogsite/programmer/static/programmer/css/styles_w.css +++ b/OneCprogsite/programmer/static/programmer/css/styles_w.css @@ -1,1406 +1,1406 @@ -/* ===== CSS RESET & VARIABLES ===== */ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -:root { - /* 1C Brand Colors - Modern Gradient Palette */ - --primary: #FF6B00; /* 1C Orange */ - --primary-dark: #E55A00; - --primary-light: #FF8A3D; - --secondary: #0055A5; /* 1C Blue */ - --secondary-dark: #004488; - --secondary-light: #3377CC; - --accent: #00A8FF; - - /* Extended 1C Color Palette */ - --gradient-primary: linear-gradient(135deg, #FF6B00 0%, #FF8A3D 100%); - --gradient-secondary: linear-gradient(135deg, #0055A5 0%, #3377CC 100%); - --gradient-hero: linear-gradient(135deg, #FF6B00 0%, #0055A5 100%); - - --text-primary: #1A1F36; - --text-secondary: #4A5568; - --text-light: #718096; - - --bg-primary: #FFFFFF; - --bg-secondary: #F7FAFC; - --bg-tertiary: #EDF2F7; - - --border-light: #E2E8F0; - --border-medium: #CBD5E0; - - /* Modern Shadows */ - --shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); - --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); - --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); - --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); - - /* Border radius */ - --radius-sm: 8px; - --radius-md: 12px; - --radius-lg: 16px; - --radius-xl: 20px; - - /* Transitions */ - --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - --transition-slow: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); -} - -/* ===== BASE STYLES ===== */ -html { - scroll-behavior: smooth; -} - -body { - font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; - line-height: 1.6; - color: var(--text-primary); - background: linear-gradient(135deg, #F7FAFC 0%, #EDF2F7 100%); - font-size: 16px; - overflow-x: hidden; -} - -.container { - max-width: 1200px; - margin: 0 auto; - padding: 0 20px; -} - -/* ===== ENHANCED HEADER ===== */ -.header { - background: rgba(255, 255, 255, 0.95); - border-bottom: 1px solid var(--border-light); - box-shadow: var(--shadow-sm); - position: sticky; - top: 0; - z-index: 1000; - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); -} - -.nav { - display: flex; - align-items: center; - justify-content: space-between; - padding: 1rem 0; - gap: 2rem; -} - -.logo { - display: flex; - align-items: center; - text-decoration: none; - font-weight: 700; - font-size: 1.375rem; - color: var(--text-primary); - transition: var(--transition); - background: var(--gradient-primary); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.logo:hover { - transform: translateY(-1px); -} - -.logo-img { - width: 40px; - height: 40px; - margin-right: 12px; - border-radius: var(--radius-md); - background: var(--gradient-primary); - padding: 4px; -} - -.nav-menu { - display: flex; - list-style: none; - gap: 2.5rem; - margin: 0; - flex-wrap: wrap; -} - -.nav-link { - text-decoration: none; - color: var(--text-secondary); - font-weight: 600; - padding: 0.75rem 0; - position: relative; - transition: var(--transition); - font-size: 1rem; -} - -.nav-link:hover { - color: var(--primary); -} - -.nav-link.active { - color: var(--primary); - font-weight: 700; -} - -.nav-link.active::after { - content: ''; - position: absolute; - bottom: -2px; - left: 0; - right: 0; - height: 3px; - background: var(--gradient-primary); - border-radius: 2px; -} - -.telegram-btn { - display: flex; - align-items: center; - gap: 10px; - background: var(--gradient-primary); - color: white; - text-decoration: none; - padding: 0.6rem 1.2rem; - border-radius: var(--radius-lg); - font-weight: 600; - transition: var(--transition); - white-space: nowrap; - box-shadow: var(--shadow-md); - border: none; - cursor: pointer; - font-size: 0.95rem; -} - -.telegram-btn:hover { - transform: translateY(-2px); - box-shadow: var(--shadow-lg); - background: var(--gradient-primary); -} - -/* ===== ENHANCED HERO SECTION ===== */ -.hero-section { - text-align: center; - padding: 4rem 0 3rem; - background: var(--gradient-hero); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; - margin-bottom: 3rem; -} - -.hero-title { - font-size: 3.5rem; - font-weight: 800; - margin-bottom: 1.5rem; - line-height: 1.1; - background: var(--gradient-hero); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.hero-subtitle { - font-size: 1.375rem; - color: var(--text-secondary); - font-weight: 500; - max-width: 600px; - margin: 0 auto; - line-height: 1.6; -} - -/* ===== MODERN CARD STYLES ===== */ -.modern-card { - background: var(--bg-primary); - border-radius: var(--radius-xl); - padding: 2rem; - box-shadow: var(--shadow-lg); - border: 1px solid var(--border-light); - transition: var(--transition-slow); - margin-bottom: 2rem; - position: relative; - overflow: hidden; -} - -.modern-card::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 4px; - background: var(--gradient-primary); -} - -.modern-card:hover { - transform: translateY(-8px); - box-shadow: var(--shadow-xl); -} - -.modern-card.secondary::before { - background: var(--gradient-secondary); -} - -.card-header { - display: flex; - align-items: flex-start; - justify-content: space-between; - margin-bottom: 1.5rem; - gap: 1rem; -} - -.card-title { - font-size: 1.5rem; - font-weight: 700; - color: var(--text-primary); - margin-bottom: 0.75rem; - background: var(--gradient-primary); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.card-subtitle { - color: var(--text-secondary); - font-size: 1rem; - margin-bottom: 1.5rem; - line-height: 1.6; -} - -.card-content { - color: var(--text-primary); - line-height: 1.7; - font-size: 1.05rem; -} - -.card-content p { - margin-bottom: 1rem; -} - -.card-actions { - margin-top: 2rem; - display: flex; - gap: 1rem; - flex-wrap: wrap; -} - -/* ===== GRID LAYOUTS ===== */ -.grid { - display: grid; - gap: 2rem; -} - -.grid-2 { - grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); -} - -.grid-3 { - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); -} - -/* ===== ENHANCED BUTTONS ===== */ -.btn { - display: inline-flex; - align-items: center; - gap: 0.75rem; - padding: 1rem 2rem; - border-radius: var(--radius-lg); - text-decoration: none; - font-weight: 600; - border: none; - cursor: pointer; - transition: var(--transition); - font-size: 1rem; - position: relative; - overflow: hidden; -} - -.btn::before { - content: ''; - position: absolute; - top: 0; - left: -100%; - width: 100%; - height: 100%; - background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent); - transition: left 0.5s; -} - -.btn:hover::before { - left: 100%; -} - -.btn-primary { - background: var(--gradient-primary); - color: white; - box-shadow: var(--shadow-md); -} - -.btn-primary:hover { - transform: translateY(-2px); - box-shadow: var(--shadow-lg); -} - -.btn-secondary { - background: var(--gradient-secondary); - color: white; - box-shadow: var(--shadow-md); -} - -.btn-secondary:hover { - transform: translateY(-2px); - box-shadow: var(--shadow-lg); -} - -.btn-outline { - background: transparent; - color: var(--primary); - border: 2px solid var(--primary); -} - -.btn-outline:hover { - background: var(--primary); - color: white; - transform: translateY(-2px); -} - -/* ===== ENHANCED ABOUT PAGE ===== */ -.about-header { - text-align: center; - margin-bottom: 3rem; - padding: 2rem 0; -} - -.about-header h2 { - font-size: 2.5rem; - font-weight: 800; - margin-bottom: 0.5rem; - background: var(--gradient-hero); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.about-header .subtitle { - font-size: 1.25rem; - color: var(--text-secondary); - font-weight: 500; -} - -.about-section { - margin-bottom: 3rem; - padding: 2.5rem; - background: var(--bg-primary); - border-radius: var(--radius-xl); - box-shadow: var(--shadow-lg); - border-left: 4px solid var(--primary); -} - -.about-section h3 { - font-size: 1.5rem; - font-weight: 700; - margin-bottom: 1.5rem; - color: var(--text-primary); - display: flex; - align-items: center; - gap: 0.75rem; -} - -.skills-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); - gap: 2rem; - margin-top: 1.5rem; -} - -.skill-category { - background: var(--bg-secondary); - padding: 1.5rem; - border-radius: var(--radius-lg); - border: 1px solid var(--border-light); - transition: var(--transition); -} - -.skill-category:hover { - transform: translateY(-4px); - box-shadow: var(--shadow-md); -} - -.skill-category h4 { - color: var(--primary); - font-weight: 600; - margin-bottom: 1rem; - font-size: 1.125rem; -} - -/* ===== ENHANCED COMPETENCE STYLES ===== */ -.competence-grid { - display: grid; - gap: 2rem; -} - -.competence-item { - display: flex; - gap: 2rem; - align-items: flex-start; - padding: 2rem; - background: var(--bg-primary); - border-radius: var(--radius-xl); - box-shadow: var(--shadow-lg); - border-left: 4px solid var(--secondary); - transition: var(--transition); -} - -.competence-item:hover { - transform: translateX(8px); - box-shadow: var(--shadow-xl); -} - -.competence-scan-wrapper { - flex-shrink: 0; -} - -.competence-scan-container { - width: 220px; - cursor: pointer; - border-radius: var(--radius-lg); - overflow: hidden; - border: 2px solid var(--border-light); - transition: var(--transition); - position: relative; - box-shadow: var(--shadow-md); -} - -.competence-scan-container:hover { - transform: translateY(-4px) scale(1.02); - box-shadow: var(--shadow-xl); - border-color: var(--primary); -} - -.competence-scan { - width: 100%; - height: auto; - display: block; - transition: var(--transition); -} - -.scan-hint { - position: absolute; - bottom: 0; - left: 0; - right: 0; - background: linear-gradient(transparent, rgba(0,0,0,0.8)); - color: white; - padding: 1rem; - text-align: center; - opacity: 0; - transition: var(--transition); - transform: translateY(10px); -} - -.competence-scan-container:hover .scan-hint { - opacity: 1; - transform: translateY(0); -} - -/* ===== ENHANCED RECALL STYLES ===== */ -.recall-grid { - display: grid; - gap: 2rem; -} - -.recall-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - margin-bottom: 1.5rem; - padding-bottom: 1.5rem; - border-bottom: 2px solid var(--border-light); -} - -.recall-title { - font-size: 1.375rem; - font-weight: 700; - color: var(--text-primary); - margin-bottom: 0.75rem; -} - -.recall-meta { - display: flex; - gap: 1rem; - font-size: 0.875rem; - font-weight: 500; -} - -.recall-date { - background: var(--gradient-primary); - color: white; - padding: 0.5rem 1rem; - border-radius: var(--radius-md); - font-weight: 600; -} - -.recall-client { - background: var(--gradient-secondary); - color: white; - padding: 0.5rem 1rem; - border-radius: var(--radius-md); - font-weight: 600; -} - -/* ===== ENHANCED PAGE HEADERS ===== */ -.page-header { - margin-bottom: 3rem; - padding: 3rem 0; - text-align: center; - background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%); - border-radius: var(--radius-xl); - box-shadow: var(--shadow-md); - border: 1px solid var(--border-light); -} - -.page-title { - font-size: 3rem; - font-weight: 800; - color: var(--text-primary); - margin-bottom: 1rem; - background: var(--gradient-hero); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.page-subtitle { - color: var(--text-secondary); - font-size: 1.25rem; - font-weight: 500; - max-width: 600px; - margin: 0 auto; -} - -/* ===== ENHANCED FOOTER ===== */ -.footer { - background: linear-gradient(135deg, var(--text-primary) 0%, #2D3748 100%); - color: white; - padding: 4rem 0 2rem; - margin-top: 6rem; - position: relative; -} - -.footer::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 4px; - background: var(--gradient-primary); -} - -.footer-content { - display: grid; - grid-template-columns: 1fr 1fr 1fr; - gap: 3rem; - margin-bottom: 3rem; -} - -.footer-info h3 { - margin-bottom: 1rem; - color: white; - font-size: 1.5rem; - font-weight: 700; -} - -.footer-info p { - opacity: 0.9; - font-size: 1.05rem; -} - -.footer-contacts p { - margin-bottom: 0.75rem; - opacity: 0.9; - display: flex; - align-items: center; - gap: 0.5rem; - font-size: 1.05rem; -} - -.footer-copyright { - grid-column: 1 / -1; - text-align: center; - padding-top: 2rem; - border-top: 1px solid rgba(255, 255, 255, 0.2); - opacity: 0.7; - font-size: 0.95rem; -} - -/* ===== ANIMATIONS ===== */ -@keyframes fadeInUp { - from { - opacity: 0; - transform: translateY(40px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -@keyframes float { - 0%, 100% { - transform: translateY(0); - } - 50% { - transform: translateY(-10px); - } -} - -@keyframes gradientShift { - 0% { - background-position: 0% 50%; - } - 50% { - background-position: 100% 50%; - } - 100% { - background-position: 0% 50%; - } -} - -.fade-in { - animation: fadeInUp 0.8s ease-out; -} - -.float { - animation: float 3s ease-in-out infinite; -} - -.gradient-animate { - background-size: 200% 200%; - animation: gradientShift 3s ease infinite; -} - -/* ===== RESPONSIVE DESIGN ===== */ -@media (max-width: 1024px) { - .nav-menu { - gap: 1.5rem; - } - - .nav-actions { - gap: 0.75rem; - } -} - -@media (max-width: 768px) { - .nav-menu { - display: none !important; - } - - .mobile-menu-btn { - display: block !important; - } - - .nav-actions .telegram-btn span:not(.telegram-icon), - .nav-actions .telegram-btn .telegram-icon { - display: none; - } - - .nav-actions .telegram-btn { - padding: 0.75rem; - width: 45px; - height: 45px; - display: flex; - align-items: center; - justify-content: center; - background: var(--primary); - color: white; - border-radius: 8px; - text-decoration: none; - } - - .nav-actions .telegram-btn::after { - content: "📱"; - font-size: 1.2rem; - } - - .theme-switcher { - display: none; - } - - .nav { - gap: 1rem; - } - - .logo-text { - font-size: 1.1rem; - } -} - -@media (max-width: 480px) { - .container { - padding: 0 12px; - } - - .nav { - padding: 0.75rem 0; - } - - .logo-img { - width: 32px; - height: 32px; - } - - .logo-text { - font-size: 1rem; - } - - .mobile-menu { - width: 280px; - } - - .nav-actions { - gap: 0.5rem; - } - - .nav-actions .telegram-btn { - width: 40px; - height: 40px; - padding: 0.5rem; - } -} - -@media (min-width: 769px) { - .mobile-menu-btn, - .mobile-menu-overlay, - .mobile-menu { - display: none !important; - } -} - -/* ===== UTILITY CLASSES ===== */ -.text-center { text-align: center; } -.text-left { text-align: left; } -.text-right { text-align: right; } - -.justify-center { justify-content: center; } -.justify-between { justify-content: space-between; } - -.mb-1 { margin-bottom: 0.5rem; } -.mb-2 { margin-bottom: 1rem; } -.mb-3 { margin-bottom: 1.5rem; } -.mb-4 { margin-bottom: 2rem; } - -.mt-1 { margin-top: 0.5rem; } -.mt-2 { margin-top: 1rem; } -.mt-3 { margin-top: 1.5rem; } -.mt-4 { margin-top: 2rem; } - -.hidden { display: none; } - -/* ===== CUSTOM COMPONENTS ===== */ -.content-card { - background: var(--bg-primary); - border-radius: var(--radius-xl); - padding: 2.5rem; - box-shadow: var(--shadow-lg); - border: 1px solid var(--border-light); - margin-bottom: 2rem; - position: relative; - overflow: hidden; -} - -.content-card::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 4px; - background: var(--gradient-primary); -} - -.improved-list { - list-style: none; - padding: 0; -} - -.improved-list li { - margin-bottom: 2rem; -} - -/* ===== FORM STYLES ===== */ -.form-group { - margin-bottom: 1.5rem; -} - -.form-group label { - display: block; - margin-bottom: 0.5rem; - font-weight: 600; - color: var(--text-primary); -} - -.form-input, .form-textarea { - width: 100%; - padding: 1rem 1.25rem; - border: 2px solid var(--border-light); - border-radius: var(--radius-lg); - font-size: 1rem; - transition: var(--transition); - background: var(--bg-primary); - font-family: inherit; -} - -.form-input:focus, .form-textarea:focus { - outline: none; - border-color: var(--primary); - box-shadow: 0 0 0 4px rgba(255, 107, 0, 0.1); - transform: translateY(-2px); -} - -.form-textarea { - resize: vertical; - min-height: 120px; - line-height: 1.5; -} - -/* ===== MODAL ENHANCEMENTS ===== */ -.modal { - display: none; - position: fixed; - z-index: 1000; - left: 0; - top: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.8); - backdrop-filter: blur(10px); - animation: fadeIn 0.3s ease; -} - -.modal-content { - background: var(--bg-primary); - margin: 2% auto; - border-radius: var(--radius-xl); - box-shadow: var(--shadow-xl); - animation: slideIn 0.3s ease; - position: relative; - max-width: 90%; - max-height: 90vh; - overflow: auto; -} - -.modal-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 2rem 2rem 1rem; - border-bottom: 2px solid var(--border-light); -} - -.modal-header h3 { - margin: 0; - color: var(--text-primary); - font-size: 1.5rem; - font-weight: 700; - background: var(--gradient-primary); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.modal-body { - padding: 2rem; -} - -.modal-close { - color: var(--text-light); - font-size: 2rem; - font-weight: bold; - cursor: pointer; - transition: var(--transition); - background: none; - border: none; - padding: 0; - width: 40px; - height: 40px; - display: flex; - align-items: center; - justify-content: center; - border-radius: var(--radius-md); -} - -.modal-close:hover { - color: var(--primary); - background: var(--bg-secondary); - transform: rotate(90deg); -} - -@keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } -} - -@keyframes slideIn { - from { - opacity: 0; - transform: translateY(-50px) scale(0.9); - } - to { - opacity: 1; - transform: translateY(0) scale(1); - } -} - -/* ===== SOLUTION PAGE STYLES ===== */ -.solution-accordion { - margin: 2rem 0; -} - -.accordion-item { - background: var(--bg-secondary); - border-radius: var(--radius-lg); - margin-bottom: 1rem; - border: 2px solid var(--border-light); - overflow: hidden; - transition: var(--transition); -} - -.accordion-item:hover { - border-color: var(--primary-light); - transform: translateX(4px); -} - -.accordion-header { - padding: 1.5rem; - background: var(--gradient-primary); - color: white; - font-weight: 600; - font-size: 1.125rem; - cursor: pointer; - display: flex; - justify-content: space-between; - align-items: center; - transition: var(--transition); -} - -.accordion-header:hover { - background: var(--primary-dark); -} - -.accordion-content { - padding: 1.5rem; - background: var(--bg-primary); - line-height: 1.7; -} - -.accordion-content p { - margin-bottom: 1rem; -} - -.accordion-content p:last-child { - margin-bottom: 0; -} - - -/* ===== THEME SWITCHER STYLES ===== */ -.theme-switcher { - display: flex; - align-items: center; -} - -.theme-toggle-checkbox { - display: none; -} - -.theme-toggle-label { - position: relative; - display: flex; - align-items: center; - width: 60px; - height: 30px; - background: var(--bg-card); - border: 2px solid var(--border-light); - border-radius: 25px; - cursor: pointer; - transition: all 0.3s ease; - padding: 2px; - box-shadow: var(--shadow-sm); -} - -.theme-toggle-label:hover { - border-color: var(--primary); - box-shadow: 0 0 10px rgba(255, 107, 0, 0.3); -} - -.theme-toggle-slider { - position: absolute; - width: 24px; - height: 24px; - background: var(--primary); - border-radius: 50%; - transition: all 0.3s ease; - left: 2px; - z-index: 2; -} - -.theme-toggle-checkbox:checked + .theme-toggle-label .theme-toggle-slider { - transform: translateX(30px); - background: var(--secondary); -} - -.theme-icon { - position: absolute; - font-size: 12px; - transition: all 0.3s ease; - z-index: 1; -} - -.theme-icon.sun { - left: 8px; - opacity: 1; -} - -.theme-icon.moon { - right: 8px; - opacity: 0; -} - -.theme-toggle-checkbox:checked + .theme-toggle-label .theme-icon.sun { - opacity: 0; -} - -.theme-toggle-checkbox:checked + .theme-toggle-label .theme-icon.moon { - opacity: 1; -} - -/* ===== MOBILE RESPONSIVE STYLES ===== */ -@media (max-width: 768px) { - .hero-title { - font-size: 2.5rem; - } - - .hero-subtitle { - font-size: 1.125rem; - padding: 0 1rem; - } - - .page-title { - font-size: 2.25rem; - } - - .page-subtitle { - font-size: 1.125rem; - padding: 0 1rem; - } - - .grid-2 { - grid-template-columns: 1fr; - gap: 1.5rem; - } - - .modern-card { - padding: 1.5rem; - margin-bottom: 1.5rem; - } - - .card-title { - font-size: 1.375rem; - } - - .content-card { - padding: 1.5rem; - } - - .about-section { - padding: 1.5rem; - } - - .skills-grid { - grid-template-columns: 1fr; - gap: 1.5rem; - } - - .competence-item { - flex-direction: column; - padding: 1.5rem; - gap: 1.5rem; - } - - .competence-scan-container { - width: 100%; - max-width: 280px; - margin: 0 auto; - } - - .recall-header { - flex-direction: column; - gap: 1rem; - align-items: flex-start; - } - - .recall-meta { - flex-direction: column; - gap: 0.5rem; - } - - .footer-content { - grid-template-columns: 1fr; - gap: 2rem; - text-align: center; - } - - .footer-contacts p { - justify-content: center; - } - - .card-actions { - flex-direction: column; - } - - .btn { - width: 100%; - justify-content: center; - } - - .modal-content { - margin: 5% auto; - max-width: 95%; - } - - .modal-header { - padding: 1.5rem 1.5rem 1rem; - } - - .modal-body { - padding: 1.5rem; - } - - .page-content { - padding: 1.5rem; - } - - .breadcrumbs { - font-size: 0.8rem; - } -} - -@media (max-width: 480px) { - .hero-title { - font-size: 2rem; - } - - .hero-subtitle { - font-size: 1rem; - } - - .page-title { - font-size: 1.875rem; - } - - .modern-card { - padding: 1.25rem; - } - - .content-card { - padding: 1.25rem; - } - - .about-section { - padding: 1.25rem; - } - - .card-title { - font-size: 1.25rem; - } - - .competence-scan-container { - max-width: 100%; - } -} - -/* Явные стили для мобильного меню в светлой теме */ -.mobile-menu { - background: #FFFFFF !important; -} - -.mobile-menu-header { - border-bottom: 1px solid #E2E8F0 !important; -} - -.mobile-menu-close { - color: #1A1F36 !important; -} - -.mobile-menu-close:hover { - background: #F7FAFC !important; -} - -.mobile-nav-link { - color: #1A1F36 !important; -} - -.mobile-nav-link:hover, -.mobile-nav-link.active { - background: var(--primary) !important; - color: white !important; -} - -.mobile-nav-actions { - border-top: 1px solid #E2E8F0 !important; -} - -/* Стили для переключателя темы в мобильном меню */ -.mobile-menu .theme-toggle-label { - background: #F7FAFC !important; - border: 2px solid #CBD5E0 !important; -} - -.mobile-menu .theme-toggle-label:hover { - border-color: var(--primary) !important; -} - -.mobile-menu .theme-icon.sun { - color: #1A1F36 !important; -} - -.mobile-menu .theme-icon.moon { - color: #1A1F36 !important; -} - -/* Стили для страницы отзывов */ -.recall-item { - display: flex; - flex-direction: column; - gap: 1.5rem; -} - -.recall-content { - display: flex; - gap: 2rem; - align-items: flex-start; -} - -.recall-scan-wrapper { - flex-shrink: 0; -} - -.recall-scan-container { - width: 280px; - cursor: pointer; - border-radius: var(--radius-lg); - overflow: hidden; - border: 2px solid var(--border-light); - transition: var(--transition); - position: relative; - box-shadow: var(--shadow-md); -} - -.recall-scan-container:hover { - transform: translateY(-4px) scale(1.02); - box-shadow: var(--shadow-xl); - border-color: var(--primary); -} - -.recall-scan { - width: 100%; - height: auto; - display: block; - transition: var(--transition); -} - -.recall-text { - flex: 1; - min-width: 0; - line-height: 1.7; - font-size: 1.05rem; -} - -.scan-hint { - position: absolute; - bottom: 0; - left: 0; - right: 0; - background: linear-gradient(transparent, rgba(0,0,0,0.8)); - color: white; - padding: 1rem; - text-align: center; - opacity: 0; - transition: var(--transition); - transform: translateY(10px); -} - -.recall-scan-container:hover .scan-hint { - opacity: 1; - transform: translateY(0); -} - -/* Стили для модального окна с изображением */ -.modal-image { - max-width: 90vw; - max-height: 80vh; - width: auto; - height: auto; - display: block; - margin: 0 auto; - border-radius: var(--radius-md); - box-shadow: var(--shadow-xl); - transition: all 0.3s ease; -} - -.modal-content.image-modal { - max-width: 95vw; - max-height: 95vh; - background: transparent; - border: none; - box-shadow: none; -} - -.modal-body.image-modal-body { - padding: 1rem; - display: flex; - align-items: center; - justify-content: center; - background: transparent; -} - -/* Адаптивность для мобильных устройств */ -@media (max-width: 768px) { - .recall-content { - flex-direction: column; - gap: 1.5rem; - } - - .recall-scan-container { - width: 100%; - max-width: 300px; - margin: 0 auto; - } - - .recall-scan-wrapper { - order: -1; - } - - .modal-image { - max-width: 95vw; - max-height: 70vh; - } - - .modal-content.image-modal { - margin: 10% auto; - } -} - -@media (max-width: 480px) { - .recall-scan-container { - max-width: 100%; - } - - .modal-content.image-modal { - margin: 5% auto; - max-width: 98vw; - } - - .modal-body.image-modal-body { - padding: 0.5rem; - } +/* ===== CSS RESET & VARIABLES ===== */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + /* 1C Brand Colors - Modern Gradient Palette */ + --primary: #FF6B00; /* 1C Orange */ + --primary-dark: #E55A00; + --primary-light: #FF8A3D; + --secondary: #0055A5; /* 1C Blue */ + --secondary-dark: #004488; + --secondary-light: #3377CC; + --accent: #00A8FF; + + /* Extended 1C Color Palette */ + --gradient-primary: linear-gradient(135deg, #FF6B00 0%, #FF8A3D 100%); + --gradient-secondary: linear-gradient(135deg, #0055A5 0%, #3377CC 100%); + --gradient-hero: linear-gradient(135deg, #FF6B00 0%, #0055A5 100%); + + --text-primary: #1A1F36; + --text-secondary: #4A5568; + --text-light: #718096; + + --bg-primary: #FFFFFF; + --bg-secondary: #F7FAFC; + --bg-tertiary: #EDF2F7; + + --border-light: #E2E8F0; + --border-medium: #CBD5E0; + + /* Modern Shadows */ + --shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); + + /* Border radius */ + --radius-sm: 8px; + --radius-md: 12px; + --radius-lg: 16px; + --radius-xl: 20px; + + /* Transitions */ + --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + --transition-slow: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); +} + +/* ===== BASE STYLES ===== */ +html { + scroll-behavior: smooth; +} + +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + line-height: 1.6; + color: var(--text-primary); + background: linear-gradient(135deg, #F7FAFC 0%, #EDF2F7 100%); + font-size: 16px; + overflow-x: hidden; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 20px; +} + +/* ===== ENHANCED HEADER ===== */ +.header { + background: rgba(255, 255, 255, 0.95); + border-bottom: 1px solid var(--border-light); + box-shadow: var(--shadow-sm); + position: sticky; + top: 0; + z-index: 1000; + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); +} + +.nav { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem 0; + gap: 2rem; +} + +.logo { + display: flex; + align-items: center; + text-decoration: none; + font-weight: 700; + font-size: 1.375rem; + color: var(--text-primary); + transition: var(--transition); + background: var(--gradient-primary); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.logo:hover { + transform: translateY(-1px); +} + +.logo-img { + width: 40px; + height: 40px; + margin-right: 12px; + border-radius: var(--radius-md); + background: var(--gradient-primary); + padding: 4px; +} + +.nav-menu { + display: flex; + list-style: none; + gap: 2.5rem; + margin: 0; + flex-wrap: wrap; +} + +.nav-link { + text-decoration: none; + color: var(--text-secondary); + font-weight: 600; + padding: 0.75rem 0; + position: relative; + transition: var(--transition); + font-size: 1rem; +} + +.nav-link:hover { + color: var(--primary); +} + +.nav-link.active { + color: var(--primary); + font-weight: 700; +} + +.nav-link.active::after { + content: ''; + position: absolute; + bottom: -2px; + left: 0; + right: 0; + height: 3px; + background: var(--gradient-primary); + border-radius: 2px; +} + +.telegram-btn { + display: flex; + align-items: center; + gap: 10px; + background: var(--gradient-primary); + color: white; + text-decoration: none; + padding: 0.6rem 1.2rem; + border-radius: var(--radius-lg); + font-weight: 600; + transition: var(--transition); + white-space: nowrap; + box-shadow: var(--shadow-md); + border: none; + cursor: pointer; + font-size: 0.95rem; +} + +.telegram-btn:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-lg); + background: var(--gradient-primary); +} + +/* ===== ENHANCED HERO SECTION ===== */ +.hero-section { + text-align: center; + padding: 4rem 0 3rem; + background: var(--gradient-hero); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + margin-bottom: 3rem; +} + +.hero-title { + font-size: 3.5rem; + font-weight: 800; + margin-bottom: 1.5rem; + line-height: 1.1; + background: var(--gradient-hero); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.hero-subtitle { + font-size: 1.375rem; + color: var(--text-secondary); + font-weight: 500; + max-width: 600px; + margin: 0 auto; + line-height: 1.6; +} + +/* ===== MODERN CARD STYLES ===== */ +.modern-card { + background: var(--bg-primary); + border-radius: var(--radius-xl); + padding: 2rem; + box-shadow: var(--shadow-lg); + border: 1px solid var(--border-light); + transition: var(--transition-slow); + margin-bottom: 2rem; + position: relative; + overflow: hidden; +} + +.modern-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: var(--gradient-primary); +} + +.modern-card:hover { + transform: translateY(-8px); + box-shadow: var(--shadow-xl); +} + +.modern-card.secondary::before { + background: var(--gradient-secondary); +} + +.card-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + margin-bottom: 1.5rem; + gap: 1rem; +} + +.card-title { + font-size: 1.5rem; + font-weight: 700; + color: var(--text-primary); + margin-bottom: 0.75rem; + background: var(--gradient-primary); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.card-subtitle { + color: var(--text-secondary); + font-size: 1rem; + margin-bottom: 1.5rem; + line-height: 1.6; +} + +.card-content { + color: var(--text-primary); + line-height: 1.7; + font-size: 1.05rem; +} + +.card-content p { + margin-bottom: 1rem; +} + +.card-actions { + margin-top: 2rem; + display: flex; + gap: 1rem; + flex-wrap: wrap; +} + +/* ===== GRID LAYOUTS ===== */ +.grid { + display: grid; + gap: 2rem; +} + +.grid-2 { + grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); +} + +.grid-3 { + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); +} + +/* ===== ENHANCED BUTTONS ===== */ +.btn { + display: inline-flex; + align-items: center; + gap: 0.75rem; + padding: 1rem 2rem; + border-radius: var(--radius-lg); + text-decoration: none; + font-weight: 600; + border: none; + cursor: pointer; + transition: var(--transition); + font-size: 1rem; + position: relative; + overflow: hidden; +} + +.btn::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent); + transition: left 0.5s; +} + +.btn:hover::before { + left: 100%; +} + +.btn-primary { + background: var(--gradient-primary); + color: white; + box-shadow: var(--shadow-md); +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-lg); +} + +.btn-secondary { + background: var(--gradient-secondary); + color: white; + box-shadow: var(--shadow-md); +} + +.btn-secondary:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-lg); +} + +.btn-outline { + background: transparent; + color: var(--primary); + border: 2px solid var(--primary); +} + +.btn-outline:hover { + background: var(--primary); + color: white; + transform: translateY(-2px); +} + +/* ===== ENHANCED ABOUT PAGE ===== */ +.about-header { + text-align: center; + margin-bottom: 3rem; + padding: 2rem 0; +} + +.about-header h2 { + font-size: 2.5rem; + font-weight: 800; + margin-bottom: 0.5rem; + background: var(--gradient-hero); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.about-header .subtitle { + font-size: 1.25rem; + color: var(--text-secondary); + font-weight: 500; +} + +.about-section { + margin-bottom: 3rem; + padding: 2.5rem; + background: var(--bg-primary); + border-radius: var(--radius-xl); + box-shadow: var(--shadow-lg); + border-left: 4px solid var(--primary); +} + +.about-section h3 { + font-size: 1.5rem; + font-weight: 700; + margin-bottom: 1.5rem; + color: var(--text-primary); + display: flex; + align-items: center; + gap: 0.75rem; +} + +.skills-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 2rem; + margin-top: 1.5rem; +} + +.skill-category { + background: var(--bg-secondary); + padding: 1.5rem; + border-radius: var(--radius-lg); + border: 1px solid var(--border-light); + transition: var(--transition); +} + +.skill-category:hover { + transform: translateY(-4px); + box-shadow: var(--shadow-md); +} + +.skill-category h4 { + color: var(--primary); + font-weight: 600; + margin-bottom: 1rem; + font-size: 1.125rem; +} + +/* ===== ENHANCED COMPETENCE STYLES ===== */ +.competence-grid { + display: grid; + gap: 2rem; +} + +.competence-item { + display: flex; + gap: 2rem; + align-items: flex-start; + padding: 2rem; + background: var(--bg-primary); + border-radius: var(--radius-xl); + box-shadow: var(--shadow-lg); + border-left: 4px solid var(--secondary); + transition: var(--transition); +} + +.competence-item:hover { + transform: translateX(8px); + box-shadow: var(--shadow-xl); +} + +.competence-scan-wrapper { + flex-shrink: 0; +} + +.competence-scan-container { + width: 220px; + cursor: pointer; + border-radius: var(--radius-lg); + overflow: hidden; + border: 2px solid var(--border-light); + transition: var(--transition); + position: relative; + box-shadow: var(--shadow-md); +} + +.competence-scan-container:hover { + transform: translateY(-4px) scale(1.02); + box-shadow: var(--shadow-xl); + border-color: var(--primary); +} + +.competence-scan { + width: 100%; + height: auto; + display: block; + transition: var(--transition); +} + +.scan-hint { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: linear-gradient(transparent, rgba(0,0,0,0.8)); + color: white; + padding: 1rem; + text-align: center; + opacity: 0; + transition: var(--transition); + transform: translateY(10px); +} + +.competence-scan-container:hover .scan-hint { + opacity: 1; + transform: translateY(0); +} + +/* ===== ENHANCED RECALL STYLES ===== */ +.recall-grid { + display: grid; + gap: 2rem; +} + +.recall-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 1.5rem; + padding-bottom: 1.5rem; + border-bottom: 2px solid var(--border-light); +} + +.recall-title { + font-size: 1.375rem; + font-weight: 700; + color: var(--text-primary); + margin-bottom: 0.75rem; +} + +.recall-meta { + display: flex; + gap: 1rem; + font-size: 0.875rem; + font-weight: 500; +} + +.recall-date { + background: var(--gradient-primary); + color: white; + padding: 0.5rem 1rem; + border-radius: var(--radius-md); + font-weight: 600; +} + +.recall-client { + background: var(--gradient-secondary); + color: white; + padding: 0.5rem 1rem; + border-radius: var(--radius-md); + font-weight: 600; +} + +/* ===== ENHANCED PAGE HEADERS ===== */ +.page-header { + margin-bottom: 3rem; + padding: 3rem 0; + text-align: center; + background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%); + border-radius: var(--radius-xl); + box-shadow: var(--shadow-md); + border: 1px solid var(--border-light); +} + +.page-title { + font-size: 3rem; + font-weight: 800; + color: var(--text-primary); + margin-bottom: 1rem; + background: var(--gradient-hero); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.page-subtitle { + color: var(--text-secondary); + font-size: 1.25rem; + font-weight: 500; + max-width: 600px; + margin: 0 auto; +} + +/* ===== ENHANCED FOOTER ===== */ +.footer { + background: linear-gradient(135deg, var(--text-primary) 0%, #2D3748 100%); + color: white; + padding: 4rem 0 2rem; + margin-top: 6rem; + position: relative; +} + +.footer::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: var(--gradient-primary); +} + +.footer-content { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 3rem; + margin-bottom: 3rem; +} + +.footer-info h3 { + margin-bottom: 1rem; + color: white; + font-size: 1.5rem; + font-weight: 700; +} + +.footer-info p { + opacity: 0.9; + font-size: 1.05rem; +} + +.footer-contacts p { + margin-bottom: 0.75rem; + opacity: 0.9; + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 1.05rem; +} + +.footer-copyright { + grid-column: 1 / -1; + text-align: center; + padding-top: 2rem; + border-top: 1px solid rgba(255, 255, 255, 0.2); + opacity: 0.7; + font-size: 0.95rem; +} + +/* ===== ANIMATIONS ===== */ +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(40px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes float { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-10px); + } +} + +@keyframes gradientShift { + 0% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } + 100% { + background-position: 0% 50%; + } +} + +.fade-in { + animation: fadeInUp 0.8s ease-out; +} + +.float { + animation: float 3s ease-in-out infinite; +} + +.gradient-animate { + background-size: 200% 200%; + animation: gradientShift 3s ease infinite; +} + +/* ===== RESPONSIVE DESIGN ===== */ +@media (max-width: 1024px) { + .nav-menu { + gap: 1.5rem; + } + + .nav-actions { + gap: 0.75rem; + } +} + +@media (max-width: 768px) { + .nav-menu { + display: none !important; + } + + .mobile-menu-btn { + display: block !important; + } + + .nav-actions .telegram-btn span:not(.telegram-icon), + .nav-actions .telegram-btn .telegram-icon { + display: none; + } + + .nav-actions .telegram-btn { + padding: 0.75rem; + width: 45px; + height: 45px; + display: flex; + align-items: center; + justify-content: center; + background: var(--primary); + color: white; + border-radius: 8px; + text-decoration: none; + } + + .nav-actions .telegram-btn::after { + content: "📱"; + font-size: 1.2rem; + } + + .theme-switcher { + display: none; + } + + .nav { + gap: 1rem; + } + + .logo-text { + font-size: 1.1rem; + } +} + +@media (max-width: 480px) { + .container { + padding: 0 12px; + } + + .nav { + padding: 0.75rem 0; + } + + .logo-img { + width: 32px; + height: 32px; + } + + .logo-text { + font-size: 1rem; + } + + .mobile-menu { + width: 280px; + } + + .nav-actions { + gap: 0.5rem; + } + + .nav-actions .telegram-btn { + width: 40px; + height: 40px; + padding: 0.5rem; + } +} + +@media (min-width: 769px) { + .mobile-menu-btn, + .mobile-menu-overlay, + .mobile-menu { + display: none !important; + } +} + +/* ===== UTILITY CLASSES ===== */ +.text-center { text-align: center; } +.text-left { text-align: left; } +.text-right { text-align: right; } + +.justify-center { justify-content: center; } +.justify-between { justify-content: space-between; } + +.mb-1 { margin-bottom: 0.5rem; } +.mb-2 { margin-bottom: 1rem; } +.mb-3 { margin-bottom: 1.5rem; } +.mb-4 { margin-bottom: 2rem; } + +.mt-1 { margin-top: 0.5rem; } +.mt-2 { margin-top: 1rem; } +.mt-3 { margin-top: 1.5rem; } +.mt-4 { margin-top: 2rem; } + +.hidden { display: none; } + +/* ===== CUSTOM COMPONENTS ===== */ +.content-card { + background: var(--bg-primary); + border-radius: var(--radius-xl); + padding: 2.5rem; + box-shadow: var(--shadow-lg); + border: 1px solid var(--border-light); + margin-bottom: 2rem; + position: relative; + overflow: hidden; +} + +.content-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: var(--gradient-primary); +} + +.improved-list { + list-style: none; + padding: 0; +} + +.improved-list li { + margin-bottom: 2rem; +} + +/* ===== FORM STYLES ===== */ +.form-group { + margin-bottom: 1.5rem; +} + +.form-group label { + display: block; + margin-bottom: 0.5rem; + font-weight: 600; + color: var(--text-primary); +} + +.form-input, .form-textarea { + width: 100%; + padding: 1rem 1.25rem; + border: 2px solid var(--border-light); + border-radius: var(--radius-lg); + font-size: 1rem; + transition: var(--transition); + background: var(--bg-primary); + font-family: inherit; +} + +.form-input:focus, .form-textarea:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 4px rgba(255, 107, 0, 0.1); + transform: translateY(-2px); +} + +.form-textarea { + resize: vertical; + min-height: 120px; + line-height: 1.5; +} + +/* ===== MODAL ENHANCEMENTS ===== */ +.modal { + display: none; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.8); + backdrop-filter: blur(10px); + animation: fadeIn 0.3s ease; +} + +.modal-content { + background: var(--bg-primary); + margin: 2% auto; + border-radius: var(--radius-xl); + box-shadow: var(--shadow-xl); + animation: slideIn 0.3s ease; + position: relative; + max-width: 90%; + max-height: 90vh; + overflow: auto; +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 2rem 2rem 1rem; + border-bottom: 2px solid var(--border-light); +} + +.modal-header h3 { + margin: 0; + color: var(--text-primary); + font-size: 1.5rem; + font-weight: 700; + background: var(--gradient-primary); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.modal-body { + padding: 2rem; +} + +.modal-close { + color: var(--text-light); + font-size: 2rem; + font-weight: bold; + cursor: pointer; + transition: var(--transition); + background: none; + border: none; + padding: 0; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-md); +} + +.modal-close:hover { + color: var(--primary); + background: var(--bg-secondary); + transform: rotate(90deg); +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(-50px) scale(0.9); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +/* ===== SOLUTION PAGE STYLES ===== */ +.solution-accordion { + margin: 2rem 0; +} + +.accordion-item { + background: var(--bg-secondary); + border-radius: var(--radius-lg); + margin-bottom: 1rem; + border: 2px solid var(--border-light); + overflow: hidden; + transition: var(--transition); +} + +.accordion-item:hover { + border-color: var(--primary-light); + transform: translateX(4px); +} + +.accordion-header { + padding: 1.5rem; + background: var(--gradient-primary); + color: white; + font-weight: 600; + font-size: 1.125rem; + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + transition: var(--transition); +} + +.accordion-header:hover { + background: var(--primary-dark); +} + +.accordion-content { + padding: 1.5rem; + background: var(--bg-primary); + line-height: 1.7; +} + +.accordion-content p { + margin-bottom: 1rem; +} + +.accordion-content p:last-child { + margin-bottom: 0; +} + + +/* ===== THEME SWITCHER STYLES ===== */ +.theme-switcher { + display: flex; + align-items: center; +} + +.theme-toggle-checkbox { + display: none; +} + +.theme-toggle-label { + position: relative; + display: flex; + align-items: center; + width: 60px; + height: 30px; + background: var(--bg-card); + border: 2px solid var(--border-light); + border-radius: 25px; + cursor: pointer; + transition: all 0.3s ease; + padding: 2px; + box-shadow: var(--shadow-sm); +} + +.theme-toggle-label:hover { + border-color: var(--primary); + box-shadow: 0 0 10px rgba(255, 107, 0, 0.3); +} + +.theme-toggle-slider { + position: absolute; + width: 24px; + height: 24px; + background: var(--primary); + border-radius: 50%; + transition: all 0.3s ease; + left: 2px; + z-index: 2; +} + +.theme-toggle-checkbox:checked + .theme-toggle-label .theme-toggle-slider { + transform: translateX(30px); + background: var(--secondary); +} + +.theme-icon { + position: absolute; + font-size: 12px; + transition: all 0.3s ease; + z-index: 1; +} + +.theme-icon.sun { + left: 8px; + opacity: 1; +} + +.theme-icon.moon { + right: 8px; + opacity: 0; +} + +.theme-toggle-checkbox:checked + .theme-toggle-label .theme-icon.sun { + opacity: 0; +} + +.theme-toggle-checkbox:checked + .theme-toggle-label .theme-icon.moon { + opacity: 1; +} + +/* ===== MOBILE RESPONSIVE STYLES ===== */ +@media (max-width: 768px) { + .hero-title { + font-size: 2.5rem; + } + + .hero-subtitle { + font-size: 1.125rem; + padding: 0 1rem; + } + + .page-title { + font-size: 2.25rem; + } + + .page-subtitle { + font-size: 1.125rem; + padding: 0 1rem; + } + + .grid-2 { + grid-template-columns: 1fr; + gap: 1.5rem; + } + + .modern-card { + padding: 1.5rem; + margin-bottom: 1.5rem; + } + + .card-title { + font-size: 1.375rem; + } + + .content-card { + padding: 1.5rem; + } + + .about-section { + padding: 1.5rem; + } + + .skills-grid { + grid-template-columns: 1fr; + gap: 1.5rem; + } + + .competence-item { + flex-direction: column; + padding: 1.5rem; + gap: 1.5rem; + } + + .competence-scan-container { + width: 100%; + max-width: 280px; + margin: 0 auto; + } + + .recall-header { + flex-direction: column; + gap: 1rem; + align-items: flex-start; + } + + .recall-meta { + flex-direction: column; + gap: 0.5rem; + } + + .footer-content { + grid-template-columns: 1fr; + gap: 2rem; + text-align: center; + } + + .footer-contacts p { + justify-content: center; + } + + .card-actions { + flex-direction: column; + } + + .btn { + width: 100%; + justify-content: center; + } + + .modal-content { + margin: 5% auto; + max-width: 95%; + } + + .modal-header { + padding: 1.5rem 1.5rem 1rem; + } + + .modal-body { + padding: 1.5rem; + } + + .page-content { + padding: 1.5rem; + } + + .breadcrumbs { + font-size: 0.8rem; + } +} + +@media (max-width: 480px) { + .hero-title { + font-size: 2rem; + } + + .hero-subtitle { + font-size: 1rem; + } + + .page-title { + font-size: 1.875rem; + } + + .modern-card { + padding: 1.25rem; + } + + .content-card { + padding: 1.25rem; + } + + .about-section { + padding: 1.25rem; + } + + .card-title { + font-size: 1.25rem; + } + + .competence-scan-container { + max-width: 100%; + } +} + +/* Явные стили для мобильного меню в светлой теме */ +.mobile-menu { + background: #FFFFFF !important; +} + +.mobile-menu-header { + border-bottom: 1px solid #E2E8F0 !important; +} + +.mobile-menu-close { + color: #1A1F36 !important; +} + +.mobile-menu-close:hover { + background: #F7FAFC !important; +} + +.mobile-nav-link { + color: #1A1F36 !important; +} + +.mobile-nav-link:hover, +.mobile-nav-link.active { + background: var(--primary) !important; + color: white !important; +} + +.mobile-nav-actions { + border-top: 1px solid #E2E8F0 !important; +} + +/* Стили для переключателя темы в мобильном меню */ +.mobile-menu .theme-toggle-label { + background: #F7FAFC !important; + border: 2px solid #CBD5E0 !important; +} + +.mobile-menu .theme-toggle-label:hover { + border-color: var(--primary) !important; +} + +.mobile-menu .theme-icon.sun { + color: #1A1F36 !important; +} + +.mobile-menu .theme-icon.moon { + color: #1A1F36 !important; +} + +/* Стили для страницы отзывов */ +.recall-item { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.recall-content { + display: flex; + gap: 2rem; + align-items: flex-start; +} + +.recall-scan-wrapper { + flex-shrink: 0; +} + +.recall-scan-container { + width: 280px; + cursor: pointer; + border-radius: var(--radius-lg); + overflow: hidden; + border: 2px solid var(--border-light); + transition: var(--transition); + position: relative; + box-shadow: var(--shadow-md); +} + +.recall-scan-container:hover { + transform: translateY(-4px) scale(1.02); + box-shadow: var(--shadow-xl); + border-color: var(--primary); +} + +.recall-scan { + width: 100%; + height: auto; + display: block; + transition: var(--transition); +} + +.recall-text { + flex: 1; + min-width: 0; + line-height: 1.7; + font-size: 1.05rem; +} + +.scan-hint { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: linear-gradient(transparent, rgba(0,0,0,0.8)); + color: white; + padding: 1rem; + text-align: center; + opacity: 0; + transition: var(--transition); + transform: translateY(10px); +} + +.recall-scan-container:hover .scan-hint { + opacity: 1; + transform: translateY(0); +} + +/* Стили для модального окна с изображением */ +.modal-image { + max-width: 90vw; + max-height: 80vh; + width: auto; + height: auto; + display: block; + margin: 0 auto; + border-radius: var(--radius-md); + box-shadow: var(--shadow-xl); + transition: all 0.3s ease; +} + +.modal-content.image-modal { + max-width: 95vw; + max-height: 95vh; + background: transparent; + border: none; + box-shadow: none; +} + +.modal-body.image-modal-body { + padding: 1rem; + display: flex; + align-items: center; + justify-content: center; + background: transparent; +} + +/* Адаптивность для мобильных устройств */ +@media (max-width: 768px) { + .recall-content { + flex-direction: column; + gap: 1.5rem; + } + + .recall-scan-container { + width: 100%; + max-width: 300px; + margin: 0 auto; + } + + .recall-scan-wrapper { + order: -1; + } + + .modal-image { + max-width: 95vw; + max-height: 70vh; + } + + .modal-content.image-modal { + margin: 10% auto; + } +} + +@media (max-width: 480px) { + .recall-scan-container { + max-width: 100%; + } + + .modal-content.image-modal { + margin: 5% auto; + max-width: 98vw; + } + + .modal-body.image-modal-body { + padding: 0.5rem; + } } \ No newline at end of file diff --git a/OneCprogsite/programmer/static/programmer/js/competence.js b/OneCprogsite/programmer/static/programmer/js/competence.js index 48e126b..0d9c523 100644 --- a/OneCprogsite/programmer/static/programmer/js/competence.js +++ b/OneCprogsite/programmer/static/programmer/js/competence.js @@ -1,73 +1,73 @@ -// competence.js - Скрипты для страницы компетенций -function openCompetenceModal(imageUrl, title) { - console.log('Opening competence modal with:', imageUrl); - const modal = document.getElementById('competenceModal'); - const modalImg = document.getElementById('competenceModalImage'); - const modalTitle = document.getElementById('competenceModalTitle'); - - if (modal && modalImg) { - modal.style.display = "block"; - modalImg.src = imageUrl; - if (title && modalTitle) { - modalTitle.textContent = title; - } - - // Добавляем класс для анимации - setTimeout(() => { - modal.classList.add('active'); - }, 10); - - // Подстраиваем размер изображения - adjustCompetenceModalImageSize(); - } -} - -function closeCompetenceModal() { - const modal = document.getElementById('competenceModal'); - if (modal) { - modal.classList.remove('active'); - setTimeout(() => { - modal.style.display = "none"; - }, 300); - } -} - -function adjustCompetenceModalImageSize() { - const modalImg = document.getElementById('competenceModalImage'); - - if (modalImg) { - const maxWidth = window.innerWidth * 0.9; - const maxHeight = window.innerHeight * 0.8; - - modalImg.style.maxWidth = `${maxWidth}px`; - modalImg.style.maxHeight = `${maxHeight}px`; - } -} - -// Инициализация после загрузки DOM -document.addEventListener('DOMContentLoaded', function() { - // Закрытие модального окна при клике вне изображения - document.addEventListener('click', function(event) { - const modal = document.getElementById('competenceModal'); - if (event.target === modal) { - closeCompetenceModal(); - } - }); - - // Закрытие по ESC - document.addEventListener('keydown', function(event) { - if (event.key === 'Escape') { - closeCompetenceModal(); - } - }); - - // Адаптация размера изображения при изменении размера окна - window.addEventListener('resize', function() { - const modalImg = document.getElementById('competenceModalImage'); - if (modalImg && modalImg.src) { - adjustCompetenceModalImageSize(); - } - }); - - console.log('Competence page scripts initialized'); +// competence.js - Скрипты для страницы компетенций +function openCompetenceModal(imageUrl, title) { + console.log('Opening competence modal with:', imageUrl); + const modal = document.getElementById('competenceModal'); + const modalImg = document.getElementById('competenceModalImage'); + const modalTitle = document.getElementById('competenceModalTitle'); + + if (modal && modalImg) { + modal.style.display = "block"; + modalImg.src = imageUrl; + if (title && modalTitle) { + modalTitle.textContent = title; + } + + // Добавляем класс для анимации + setTimeout(() => { + modal.classList.add('active'); + }, 10); + + // Подстраиваем размер изображения + adjustCompetenceModalImageSize(); + } +} + +function closeCompetenceModal() { + const modal = document.getElementById('competenceModal'); + if (modal) { + modal.classList.remove('active'); + setTimeout(() => { + modal.style.display = "none"; + }, 300); + } +} + +function adjustCompetenceModalImageSize() { + const modalImg = document.getElementById('competenceModalImage'); + + if (modalImg) { + const maxWidth = window.innerWidth * 0.9; + const maxHeight = window.innerHeight * 0.8; + + modalImg.style.maxWidth = `${maxWidth}px`; + modalImg.style.maxHeight = `${maxHeight}px`; + } +} + +// Инициализация после загрузки DOM +document.addEventListener('DOMContentLoaded', function() { + // Закрытие модального окна при клике вне изображения + document.addEventListener('click', function(event) { + const modal = document.getElementById('competenceModal'); + if (event.target === modal) { + closeCompetenceModal(); + } + }); + + // Закрытие по ESC + document.addEventListener('keydown', function(event) { + if (event.key === 'Escape') { + closeCompetenceModal(); + } + }); + + // Адаптация размера изображения при изменении размера окна + window.addEventListener('resize', function() { + const modalImg = document.getElementById('competenceModalImage'); + if (modalImg && modalImg.src) { + adjustCompetenceModalImageSize(); + } + }); + + console.log('Competence page scripts initialized'); }); \ No newline at end of file diff --git a/OneCprogsite/programmer/static/programmer/js/mobile-menu.js b/OneCprogsite/programmer/static/programmer/js/mobile-menu.js index c0b299e..9119f84 100644 --- a/OneCprogsite/programmer/static/programmer/js/mobile-menu.js +++ b/OneCprogsite/programmer/static/programmer/js/mobile-menu.js @@ -1,94 +1,94 @@ -// Mobile Menu Script -document.addEventListener('DOMContentLoaded', function() { - console.log('Mobile menu script loaded'); // Для отладки - - const mobileMenuBtn = document.getElementById('mobileMenuBtn'); - const mobileMenuClose = document.getElementById('mobileMenuClose'); - const mobileMenuOverlay = document.getElementById('mobileMenuOverlay'); - const mobileMenu = document.getElementById('mobileMenu'); - const mobileThemeToggle = document.getElementById('mobile-theme-toggle'); - const mainThemeToggle = document.getElementById('theme-toggle'); - - // Проверяем, что элементы существуют - if (!mobileMenuBtn || !mobileMenu) { - console.error('Mobile menu elements not found'); - return; - } - - console.log('Mobile menu elements found:', { - mobileMenuBtn, - mobileMenuClose, - mobileMenuOverlay, - mobileMenu, - mobileThemeToggle, - mainThemeToggle - }); - - // Открытие мобильного меню - mobileMenuBtn.addEventListener('click', function() { - console.log('Opening mobile menu'); - mobileMenu.classList.add('active'); - mobileMenuOverlay.style.display = 'block'; - document.body.style.overflow = 'hidden'; - }); - - // Закрытие мобильного меню - function closeMobileMenu() { - console.log('Closing mobile menu'); - mobileMenu.classList.remove('active'); - mobileMenuOverlay.style.display = 'none'; - document.body.style.overflow = ''; - } - - if (mobileMenuClose) { - mobileMenuClose.addEventListener('click', closeMobileMenu); - } - - if (mobileMenuOverlay) { - mobileMenuOverlay.addEventListener('click', closeMobileMenu); - } - - // Закрытие меню при клике на ссылку - const mobileNavLinks = document.querySelectorAll('.mobile-nav-link'); - mobileNavLinks.forEach(link => { - link.addEventListener('click', closeMobileMenu); - }); - - // Синхронизация переключателей темы - function syncThemeToggles() { - if (mobileThemeToggle && mainThemeToggle) { - mobileThemeToggle.checked = mainThemeToggle.checked; - } - } - - if (mainThemeToggle) { - mainThemeToggle.addEventListener('change', function() { - console.log('Main theme toggle changed:', this.checked); - syncThemeToggles(); - }); - } - - if (mobileThemeToggle) { - mobileThemeToggle.addEventListener('change', function() { - console.log('Mobile theme toggle changed:', this.checked); - if (mainThemeToggle) { - mainThemeToggle.checked = this.checked; - // Триггерим событие change - const event = new Event('change'); - mainThemeToggle.dispatchEvent(event); - } - }); - } - - // Инициализация синхронизации - syncThemeToggles(); - - // Закрытие меню по ESC - document.addEventListener('keydown', function(event) { - if (event.key === 'Escape') { - closeMobileMenu(); - } - }); - - console.log('Mobile menu script initialized successfully'); +// Mobile Menu Script +document.addEventListener('DOMContentLoaded', function() { + console.log('Mobile menu script loaded'); // Для отладки + + const mobileMenuBtn = document.getElementById('mobileMenuBtn'); + const mobileMenuClose = document.getElementById('mobileMenuClose'); + const mobileMenuOverlay = document.getElementById('mobileMenuOverlay'); + const mobileMenu = document.getElementById('mobileMenu'); + const mobileThemeToggle = document.getElementById('mobile-theme-toggle'); + const mainThemeToggle = document.getElementById('theme-toggle'); + + // Проверяем, что элементы существуют + if (!mobileMenuBtn || !mobileMenu) { + console.error('Mobile menu elements not found'); + return; + } + + console.log('Mobile menu elements found:', { + mobileMenuBtn, + mobileMenuClose, + mobileMenuOverlay, + mobileMenu, + mobileThemeToggle, + mainThemeToggle + }); + + // Открытие мобильного меню + mobileMenuBtn.addEventListener('click', function() { + console.log('Opening mobile menu'); + mobileMenu.classList.add('active'); + mobileMenuOverlay.style.display = 'block'; + document.body.style.overflow = 'hidden'; + }); + + // Закрытие мобильного меню + function closeMobileMenu() { + console.log('Closing mobile menu'); + mobileMenu.classList.remove('active'); + mobileMenuOverlay.style.display = 'none'; + document.body.style.overflow = ''; + } + + if (mobileMenuClose) { + mobileMenuClose.addEventListener('click', closeMobileMenu); + } + + if (mobileMenuOverlay) { + mobileMenuOverlay.addEventListener('click', closeMobileMenu); + } + + // Закрытие меню при клике на ссылку + const mobileNavLinks = document.querySelectorAll('.mobile-nav-link'); + mobileNavLinks.forEach(link => { + link.addEventListener('click', closeMobileMenu); + }); + + // Синхронизация переключателей темы + function syncThemeToggles() { + if (mobileThemeToggle && mainThemeToggle) { + mobileThemeToggle.checked = mainThemeToggle.checked; + } + } + + if (mainThemeToggle) { + mainThemeToggle.addEventListener('change', function() { + console.log('Main theme toggle changed:', this.checked); + syncThemeToggles(); + }); + } + + if (mobileThemeToggle) { + mobileThemeToggle.addEventListener('change', function() { + console.log('Mobile theme toggle changed:', this.checked); + if (mainThemeToggle) { + mainThemeToggle.checked = this.checked; + // Триггерим событие change + const event = new Event('change'); + mainThemeToggle.dispatchEvent(event); + } + }); + } + + // Инициализация синхронизации + syncThemeToggles(); + + // Закрытие меню по ESC + document.addEventListener('keydown', function(event) { + if (event.key === 'Escape') { + closeMobileMenu(); + } + }); + + console.log('Mobile menu script initialized successfully'); }); \ No newline at end of file diff --git a/OneCprogsite/programmer/static/programmer/js/recall.js b/OneCprogsite/programmer/static/programmer/js/recall.js index b8f2c6e..8d84992 100644 --- a/OneCprogsite/programmer/static/programmer/js/recall.js +++ b/OneCprogsite/programmer/static/programmer/js/recall.js @@ -1,74 +1,74 @@ -// recall.js - Скрипты для страницы отзывов -function openModal(imageUrl, title) { - console.log('Opening modal with:', imageUrl); - const modal = document.getElementById('imageModal'); - const modalImg = document.getElementById('modalImage'); - const modalTitle = document.getElementById('modalTitle'); - - if (modal && modalImg) { - modal.style.display = "block"; - modalImg.src = imageUrl; - if (title && modalTitle) { - modalTitle.textContent = title; - } - - // Добавляем класс для анимации - setTimeout(() => { - modal.classList.add('active'); - }, 10); - - // Подстраиваем размер изображения - adjustModalImageSize(); - } -} - -function closeModal() { - const modal = document.getElementById('imageModal'); - if (modal) { - modal.classList.remove('active'); - setTimeout(() => { - modal.style.display = "none"; - }, 300); - } -} - -function adjustModalImageSize() { - const modalImg = document.getElementById('modalImage'); - const modalContent = document.querySelector('.modal-content'); - - if (modalImg && modalContent) { - const maxWidth = window.innerWidth * 0.9; - const maxHeight = window.innerHeight * 0.8; - - modalImg.style.maxWidth = `${maxWidth}px`; - modalImg.style.maxHeight = `${maxHeight}px`; - } -} - -// Инициализация после загрузки DOM -document.addEventListener('DOMContentLoaded', function() { - // Закрытие модального окна при клике вне изображения - document.addEventListener('click', function(event) { - const modal = document.getElementById('imageModal'); - if (event.target === modal) { - closeModal(); - } - }); - - // Закрытие по ESC - document.addEventListener('keydown', function(event) { - if (event.key === 'Escape') { - closeModal(); - } - }); - - // Адаптация размера изображения при изменении размера окна - window.addEventListener('resize', function() { - const modalImg = document.getElementById('modalImage'); - if (modalImg && modalImg.src) { - adjustModalImageSize(); - } - }); - - console.log('Recall page scripts initialized'); +// recall.js - Скрипты для страницы отзывов +function openModal(imageUrl, title) { + console.log('Opening modal with:', imageUrl); + const modal = document.getElementById('imageModal'); + const modalImg = document.getElementById('modalImage'); + const modalTitle = document.getElementById('modalTitle'); + + if (modal && modalImg) { + modal.style.display = "block"; + modalImg.src = imageUrl; + if (title && modalTitle) { + modalTitle.textContent = title; + } + + // Добавляем класс для анимации + setTimeout(() => { + modal.classList.add('active'); + }, 10); + + // Подстраиваем размер изображения + adjustModalImageSize(); + } +} + +function closeModal() { + const modal = document.getElementById('imageModal'); + if (modal) { + modal.classList.remove('active'); + setTimeout(() => { + modal.style.display = "none"; + }, 300); + } +} + +function adjustModalImageSize() { + const modalImg = document.getElementById('modalImage'); + const modalContent = document.querySelector('.modal-content'); + + if (modalImg && modalContent) { + const maxWidth = window.innerWidth * 0.9; + const maxHeight = window.innerHeight * 0.8; + + modalImg.style.maxWidth = `${maxWidth}px`; + modalImg.style.maxHeight = `${maxHeight}px`; + } +} + +// Инициализация после загрузки DOM +document.addEventListener('DOMContentLoaded', function() { + // Закрытие модального окна при клике вне изображения + document.addEventListener('click', function(event) { + const modal = document.getElementById('imageModal'); + if (event.target === modal) { + closeModal(); + } + }); + + // Закрытие по ESC + document.addEventListener('keydown', function(event) { + if (event.key === 'Escape') { + closeModal(); + } + }); + + // Адаптация размера изображения при изменении размера окна + window.addEventListener('resize', function() { + const modalImg = document.getElementById('modalImage'); + if (modalImg && modalImg.src) { + adjustModalImageSize(); + } + }); + + console.log('Recall page scripts initialized'); }); \ No newline at end of file diff --git a/OneCprogsite/programmer/static/programmer/js/solution-accordion.js b/OneCprogsite/programmer/static/programmer/js/solution-accordion.js index 1274669..c96fb18 100644 --- a/OneCprogsite/programmer/static/programmer/js/solution-accordion.js +++ b/OneCprogsite/programmer/static/programmer/js/solution-accordion.js @@ -1,46 +1,46 @@ -// solution-accordion.js -function toggleAccordion(header) { - const content = header.nextElementSibling; - const icon = header.querySelector('.accordion-icon'); - - // Переключаем только текущий аккордеон - header.classList.toggle('active'); - content.classList.toggle('active'); - icon.style.transform = header.classList.contains('active') ? 'rotate(180deg)' : 'rotate(0deg)'; -} - -// Функция для открытия всех аккордеонов -function expandAll() { - document.querySelectorAll('.accordion-content').forEach(content => { - content.classList.add('active'); - }); - document.querySelectorAll('.accordion-header').forEach(header => { - header.classList.add('active'); - }); - document.querySelectorAll('.accordion-icon').forEach(icon => { - icon.style.transform = 'rotate(180deg)'; - }); -} - -// Функция для закрытия всех аккордеонов -function collapseAll() { - document.querySelectorAll('.accordion-content').forEach(content => { - content.classList.remove('active'); - }); - document.querySelectorAll('.accordion-header').forEach(header => { - header.classList.remove('active'); - }); - document.querySelectorAll('.accordion-icon').forEach(icon => { - icon.style.transform = 'rotate(0deg)'; - }); -} - -// Автоматически открываем первый аккордеон в каждой карточке при загрузке -document.addEventListener('DOMContentLoaded', function() { - document.querySelectorAll('.content-card').forEach(card => { - const firstAccordion = card.querySelector('.accordion-header'); - if (firstAccordion) { - toggleAccordion(firstAccordion); - } - }); +// solution-accordion.js +function toggleAccordion(header) { + const content = header.nextElementSibling; + const icon = header.querySelector('.accordion-icon'); + + // Переключаем только текущий аккордеон + header.classList.toggle('active'); + content.classList.toggle('active'); + icon.style.transform = header.classList.contains('active') ? 'rotate(180deg)' : 'rotate(0deg)'; +} + +// Функция для открытия всех аккордеонов +function expandAll() { + document.querySelectorAll('.accordion-content').forEach(content => { + content.classList.add('active'); + }); + document.querySelectorAll('.accordion-header').forEach(header => { + header.classList.add('active'); + }); + document.querySelectorAll('.accordion-icon').forEach(icon => { + icon.style.transform = 'rotate(180deg)'; + }); +} + +// Функция для закрытия всех аккордеонов +function collapseAll() { + document.querySelectorAll('.accordion-content').forEach(content => { + content.classList.remove('active'); + }); + document.querySelectorAll('.accordion-header').forEach(header => { + header.classList.remove('active'); + }); + document.querySelectorAll('.accordion-icon').forEach(icon => { + icon.style.transform = 'rotate(0deg)'; + }); +} + +// Автоматически открываем первый аккордеон в каждой карточке при загрузке +document.addEventListener('DOMContentLoaded', function() { + document.querySelectorAll('.content-card').forEach(card => { + const firstAccordion = card.querySelector('.accordion-header'); + if (firstAccordion) { + toggleAccordion(firstAccordion); + } + }); }); \ No newline at end of file diff --git a/OneCprogsite/programmer/static/programmer/js/theme-switcher.js b/OneCprogsite/programmer/static/programmer/js/theme-switcher.js index 0cd326f..4dcc5ab 100644 --- a/OneCprogsite/programmer/static/programmer/js/theme-switcher.js +++ b/OneCprogsite/programmer/static/programmer/js/theme-switcher.js @@ -1,68 +1,68 @@ -// Theme Switcher Script -document.addEventListener('DOMContentLoaded', function() { - const themeToggle = document.getElementById('theme-toggle'); - const mobileThemeToggle = document.getElementById('mobile-theme-toggle'); - const themeCSS = document.getElementById('theme-css'); - - // Проверяем сохраненную тему в localStorage - const savedTheme = localStorage.getItem('theme'); - - // Устанавливаем светлую тему по умолчанию - if (savedTheme === 'dark') { - switchToDarkTheme(); - } else { - switchToLightTheme(); // Светлая тема по умолчанию - } - - // Обработчик переключения темы для десктопного переключателя - if (themeToggle) { - themeToggle.addEventListener('change', function() { - if (this.checked) { - switchToLightTheme(); - } else { - switchToDarkTheme(); - } - }); - } - - // Обработчик переключения темы для мобильного переключателя - if (mobileThemeToggle) { - mobileThemeToggle.addEventListener('change', function() { - if (this.checked) { - switchToLightTheme(); - } else { - switchToDarkTheme(); - } - // Синхронизируем оба переключателя - if (themeToggle) { - themeToggle.checked = this.checked; - } - }); - } - - function switchToLightTheme() { - themeCSS.href = themeCSS.href.replace('styles_dark.css', 'styles_w.css'); - if (themeToggle) themeToggle.checked = true; - if (mobileThemeToggle) mobileThemeToggle.checked = true; - localStorage.setItem('theme', 'light'); - } - - function switchToDarkTheme() { - themeCSS.href = themeCSS.href.replace('styles_w.css', 'styles_dark.css'); - if (themeToggle) themeToggle.checked = false; - if (mobileThemeToggle) mobileThemeToggle.checked = false; - localStorage.setItem('theme', 'dark'); - } - - // Синхронизация переключателей при загрузке - if (themeToggle && mobileThemeToggle) { - mobileThemeToggle.checked = themeToggle.checked; - } - - // Обработка ошибок загрузки CSS - themeCSS.onerror = function() { - console.error('Ошибка загрузки CSS файла темы'); - // Восстанавливаем светлую тему по умолчанию при ошибке - themeCSS.href = themeCSS.href.replace('styles_dark.css', 'styles_w.css'); - }; +// Theme Switcher Script +document.addEventListener('DOMContentLoaded', function() { + const themeToggle = document.getElementById('theme-toggle'); + const mobileThemeToggle = document.getElementById('mobile-theme-toggle'); + const themeCSS = document.getElementById('theme-css'); + + // Проверяем сохраненную тему в localStorage + const savedTheme = localStorage.getItem('theme'); + + // Устанавливаем светлую тему по умолчанию + if (savedTheme === 'dark') { + switchToDarkTheme(); + } else { + switchToLightTheme(); // Светлая тема по умолчанию + } + + // Обработчик переключения темы для десктопного переключателя + if (themeToggle) { + themeToggle.addEventListener('change', function() { + if (this.checked) { + switchToLightTheme(); + } else { + switchToDarkTheme(); + } + }); + } + + // Обработчик переключения темы для мобильного переключателя + if (mobileThemeToggle) { + mobileThemeToggle.addEventListener('change', function() { + if (this.checked) { + switchToLightTheme(); + } else { + switchToDarkTheme(); + } + // Синхронизируем оба переключателя + if (themeToggle) { + themeToggle.checked = this.checked; + } + }); + } + + function switchToLightTheme() { + themeCSS.href = themeCSS.href.replace('styles_dark.css', 'styles_w.css'); + if (themeToggle) themeToggle.checked = true; + if (mobileThemeToggle) mobileThemeToggle.checked = true; + localStorage.setItem('theme', 'light'); + } + + function switchToDarkTheme() { + themeCSS.href = themeCSS.href.replace('styles_w.css', 'styles_dark.css'); + if (themeToggle) themeToggle.checked = false; + if (mobileThemeToggle) mobileThemeToggle.checked = false; + localStorage.setItem('theme', 'dark'); + } + + // Синхронизация переключателей при загрузке + if (themeToggle && mobileThemeToggle) { + mobileThemeToggle.checked = themeToggle.checked; + } + + // Обработка ошибок загрузки CSS + themeCSS.onerror = function() { + console.error('Ошибка загрузки CSS файла темы'); + // Восстанавливаем светлую тему по умолчанию при ошибке + themeCSS.href = themeCSS.href.replace('styles_dark.css', 'styles_w.css'); + }; }); \ No newline at end of file diff --git a/OneCprogsite/programmer/templates/admin/base.html b/OneCprogsite/programmer/templates/admin/base.html index c8f00f5..7c2ee58 100644 --- a/OneCprogsite/programmer/templates/admin/base.html +++ b/OneCprogsite/programmer/templates/admin/base.html @@ -1,79 +1,79 @@ - - - - - - {% block title %}Админ-панель - Статистика{% endblock %} - {% load django_bootstrap5 %} - {% bootstrap_css %} - - - -
-
-
-

{% block page_title %}Админ-панель{% endblock %}

- -
-
-
- -
- {% bootstrap_messages %} - {% block content %} - {% endblock %} -
- - {% bootstrap_javascript %} - + + + + + + {% block title %}Админ-панель - Статистика{% endblock %} + {% load django_bootstrap5 %} + {% bootstrap_css %} + + + +
+
+
+

{% block page_title %}Админ-панель{% endblock %}

+ +
+
+
+ +
+ {% bootstrap_messages %} + {% block content %} + {% endblock %} +
+ + {% bootstrap_javascript %} + \ No newline at end of file diff --git a/OneCprogsite/programmer/templates/admin/base_site.html b/OneCprogsite/programmer/templates/admin/base_site.html index 58bd451..c847198 100644 --- a/OneCprogsite/programmer/templates/admin/base_site.html +++ b/OneCprogsite/programmer/templates/admin/base_site.html @@ -1,26 +1,26 @@ -{% extends "admin/base.html" %} - -{% block title %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %} - -{% block branding %} -

{{ site_header|default:_('Django administration') }}

-{% endblock %} - -{% block nav-global %} -{% endblock %} - -{% block userlinks %} - {% load programmer_tags %} - - - {% get_unread_callbacks as unread_callbacks %} - {% if unread_callbacks %} - - 🚨 {{ unread_callbacks }} новых заявок - / - {% endif %} - - 📊 Статистика заявок / - 📈 Посещения / - {{ block.super }} +{% extends "admin/base.html" %} + +{% block title %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %} + +{% block branding %} +

{{ site_header|default:_('Django administration') }}

+{% endblock %} + +{% block nav-global %} +{% endblock %} + +{% block userlinks %} + {% load programmer_tags %} + + + {% get_unread_callbacks as unread_callbacks %} + {% if unread_callbacks %} + + 🚨 {{ unread_callbacks }} новых заявок + / + {% endif %} + + 📊 Статистика заявок / + 📈 Посещения / + {{ block.super }} {% endblock %} \ No newline at end of file diff --git a/OneCprogsite/programmer/templates/admin/callback_stats.html b/OneCprogsite/programmer/templates/admin/callback_stats.html index 916c167..31ee95a 100644 --- a/OneCprogsite/programmer/templates/admin/callback_stats.html +++ b/OneCprogsite/programmer/templates/admin/callback_stats.html @@ -1,45 +1,45 @@ -{% extends "admin/base_site.html" %} - -{% block title %}Статистика заявок{% endblock %} - -{% block content %} -
-

📊 Статистика заявок на обратный звонок

- -
-
-

📋 Всего заявок

-

{{ stats.total }}

-
- -
-

📅 Сегодня

-

{{ stats.today }}

-
- -
-

📈 За неделю

-

{{ stats.week }}

-
- -
-

🆕 Непрочитанные

-

{{ stats.unread }}

-
- -
-

⏳ В обработке

-

{{ stats.unprocessed }}

-
-
- -
- - 📋 Перейти к списку заявок - - - 🏠 На главную админки - -
-
+{% extends "admin/base_site.html" %} + +{% block title %}Статистика заявок{% endblock %} + +{% block content %} +
+

📊 Статистика заявок на обратный звонок

+ +
+
+

📋 Всего заявок

+

{{ stats.total }}

+
+ +
+

📅 Сегодня

+

{{ stats.today }}

+
+ +
+

📈 За неделю

+

{{ stats.week }}

+
+ +
+

🆕 Непрочитанные

+

{{ stats.unread }}

+
+ +
+

⏳ В обработке

+

{{ stats.unprocessed }}

+
+
+ +
+ + 📋 Перейти к списку заявок + + + 🏠 На главную админки + +
+
{% endblock %} \ No newline at end of file diff --git a/OneCprogsite/programmer/templates/admin/statistics.html b/OneCprogsite/programmer/templates/admin/statistics.html index 4a1650d..ae21071 100644 --- a/OneCprogsite/programmer/templates/admin/statistics.html +++ b/OneCprogsite/programmer/templates/admin/statistics.html @@ -1,131 +1,131 @@ -{% extends 'admin/base.html' %} -{% load programmer_tags %} - -{% block title %}Статистика посещений{% endblock %} -{% block page_title %}Статистика посещений{% endblock %} - -{% block content %} - -{% get_unread_callbacks as unread_callbacks %} -{% get_today_callbacks as today_callbacks %} -{% if unread_callbacks > 0 %} - -{% endif %} - -
-
-
-
-

📊 Просмотров сегодня

-

{{ today_views }}

-
-
-

📈 Просмотров за неделю

-

{{ weekly_views }}

-
-
-

👥 Уникальных посетителей

-

{{ unique_visitors }}

-
-
-

🕒 Всего просмотров

-

{{ total_views }}

-
- -
-

📞 Заявок сегодня

-

{{ today_callbacks }}

-
-
-

📋 Всего заявок

-

{% get_unread_callbacks %}/{{ total_callbacks }}

- (непрочитанные/всего) -
-
-
-
- - -
-
-
-
-

🔥 Популярные страницы (за неделю)

-
-
- - - - - - - - - {% for page in popular_pages %} - - - - - {% empty %} - - - - {% endfor %} - -
СтраницаПросмотров
- {{ page.url }} - {% if page.url == '/' %} - Главная - {% endif %} - {{ page.views }}
Нет данных за выбранный период
-
-
-
-
- -
-
-
-
-

📋 Последние посещения

-
-
- - - - - - - - - - {% for view in recent_views %} - - - - - - {% empty %} - - - - {% endfor %} - -
ВремяСтраницаIP-адрес
{{ view.timestamp|date:"d.m.Y H:i" }}{{ view.url }}{{ view.ip_address }}
Нет данных
-
-
-
-
+{% extends 'admin/base.html' %} +{% load programmer_tags %} + +{% block title %}Статистика посещений{% endblock %} +{% block page_title %}Статистика посещений{% endblock %} + +{% block content %} + +{% get_unread_callbacks as unread_callbacks %} +{% get_today_callbacks as today_callbacks %} +{% if unread_callbacks > 0 %} + +{% endif %} + +
+
+
+
+

📊 Просмотров сегодня

+

{{ today_views }}

+
+
+

📈 Просмотров за неделю

+

{{ weekly_views }}

+
+
+

👥 Уникальных посетителей

+

{{ unique_visitors }}

+
+
+

🕒 Всего просмотров

+

{{ total_views }}

+
+ +
+

📞 Заявок сегодня

+

{{ today_callbacks }}

+
+
+

📋 Всего заявок

+

{% get_unread_callbacks %}/{{ total_callbacks }}

+ (непрочитанные/всего) +
+
+
+
+ + +
+
+
+
+

🔥 Популярные страницы (за неделю)

+
+
+ + + + + + + + + {% for page in popular_pages %} + + + + + {% empty %} + + + + {% endfor %} + +
СтраницаПросмотров
+ {{ page.url }} + {% if page.url == '/' %} + Главная + {% endif %} + {{ page.views }}
Нет данных за выбранный период
+
+
+
+
+ +
+
+
+
+

📋 Последние посещения

+
+
+ + + + + + + + + + {% for view in recent_views %} + + + + + + {% empty %} + + + + {% endfor %} + +
ВремяСтраницаIP-адрес
{{ view.timestamp|date:"d.m.Y H:i" }}{{ view.url }}{{ view.ip_address }}
Нет данных
+
+
+
+
{% endblock %} \ No newline at end of file diff --git a/OneCprogsite/programmer/templates/emails/callback_notification.html b/OneCprogsite/programmer/templates/emails/callback_notification.html index 4e7cdd8..503b2df 100644 --- a/OneCprogsite/programmer/templates/emails/callback_notification.html +++ b/OneCprogsite/programmer/templates/emails/callback_notification.html @@ -1,57 +1,57 @@ - - - - - - - - -
-
-

🚨 Новая заявка на сайте

-

Требуется ваше внимание!

-
- -
-
- ⚠️ Срочно! Пользователь оставил заявку на обратный звонок. -
- -
-

📋 Информация о заявке:

-

👤 Имя: {{ callback.name }}

-

📞 Телефон: {{ callback.phone }}

- {% if callback.email %} -

📧 Email: {{ callback.email }}

- {% endif %} - {% if callback.question %} -

❓ Вопрос:
{{ callback.question }}

- {% endif %} -

🕒 Время отправки: {{ callback.time_create|date:"d.m.Y H:i" }}

-
- -
- - 📋 Перейти к заявкам в админке - -
- -

Не забудьте отметить заявку как обработанную после связи с клиентом!

-
- - -
- + + + + + + + + +
+
+

🚨 Новая заявка на сайте

+

Требуется ваше внимание!

+
+ +
+
+ ⚠️ Срочно! Пользователь оставил заявку на обратный звонок. +
+ +
+

📋 Информация о заявке:

+

👤 Имя: {{ callback.name }}

+

📞 Телефон: {{ callback.phone }}

+ {% if callback.email %} +

📧 Email: {{ callback.email }}

+ {% endif %} + {% if callback.question %} +

❓ Вопрос:
{{ callback.question }}

+ {% endif %} +

🕒 Время отправки: {{ callback.time_create|date:"d.m.Y H:i" }}

+
+ +
+ + 📋 Перейти к заявкам в админке + +
+ +

Не забудьте отметить заявку как обработанную после связи с клиентом!

+
+ + +
+ \ No newline at end of file diff --git a/OneCprogsite/programmer/templates/emails/daily_summary.html b/OneCprogsite/programmer/templates/emails/daily_summary.html index 7886975..960740f 100644 --- a/OneCprogsite/programmer/templates/emails/daily_summary.html +++ b/OneCprogsite/programmer/templates/emails/daily_summary.html @@ -1,60 +1,60 @@ - - - - - - - - -
-
-

📊 Ежедневная сводка

-

Статистика заявок за {{ yesterday|date:"d.m.Y" }}

-
- -
-
-
-

📅 Вчерашние заявки

-

{{ yesterday_callbacks }}

-
- -
-

⏳ Ожидают обработки

-

{{ unprocessed_callbacks }}

-
-
- - {% if unprocessed_callbacks > 0 %} -
- ⚠️ Внимание! У вас есть {{ unprocessed_callbacks }} необработанных заявок. -
- {% endif %} - -
- - 📋 Управление заявками - -
- -

Не забудьте обработать все pending заявки!

-
- - -
- + + + + + + + + +
+
+

📊 Ежедневная сводка

+

Статистика заявок за {{ yesterday|date:"d.m.Y" }}

+
+ +
+
+
+

📅 Вчерашние заявки

+

{{ yesterday_callbacks }}

+
+ +
+

⏳ Ожидают обработки

+

{{ unprocessed_callbacks }}

+
+
+ + {% if unprocessed_callbacks > 0 %} +
+ ⚠️ Внимание! У вас есть {{ unprocessed_callbacks }} необработанных заявок. +
+ {% endif %} + +
+ + 📋 Управление заявками + +
+ +

Не забудьте обработать все pending заявки!

+
+ + +
+ \ No newline at end of file diff --git a/OneCprogsite/programmer/templates/programmer/about.html b/OneCprogsite/programmer/templates/programmer/about.html index 696fa6d..d7f55c7 100644 --- a/OneCprogsite/programmer/templates/programmer/about.html +++ b/OneCprogsite/programmer/templates/programmer/about.html @@ -1,154 +1,154 @@ -{% extends 'programmer/base.html' %} -{% load django_bootstrap5 %} - -{% block content %} - - -
-
-

Николай Сердюк

-

Разработчик 1С

-
- -
-

🚀 Опыт работы

-

Более 10 лет успешной работы в разработке и сопровождении систем на платформе 1С

- -
-

Основные направления:

-
-
-

💻 Разработка

-
    -
  • Разработка и доработка конфигураций 1С
  • -
  • Создание внешних обработок и отчетов
  • -
  • Кастомизация под бизнес-процессы
  • -
-
-
-

🔗 Интеграция

-
    -
  • Интеграция 1С с веб-сервисами
  • -
  • Связь с сайтами и мобильными приложениями
  • -
  • API и веб-сервисы
  • -
-
-
-

⚡ Оптимизация

-
    -
  • Оптимизация бизнес-процессов
  • -
  • Ускорение работы баз данных
  • -
  • Автоматизация рутинных операций
  • -
-
-
-
-
- -
-

🛠 Технологии и навыки

-
-
-

🎯 1С Разработка

-
    -
  • 1С:Предприятие 8.3
  • -
  • Управление торговлей
  • -
  • Бухгалтерия предприятия
  • -
  • Зарплата и управление персоналом
  • -
  • Внешние обработки и отчеты
  • -
-
-
-

🔧 Дополнительные технологии

-
    -
  • SQL и оптимизация запросов
  • -
  • Веб-сервисы и API
  • -
  • XML, JSON, REST
  • -
  • Системное администрирование
  • -
-
-
-
- -
-

📈 Проекты и достижения

-

Успешно реализовал более 50 проектов различной сложности

- -
-
-

🏆 Ключевые проекты

-
    -
  • Автоматизация учетных систем для предприятий
  • -
  • Интеграция 1С с сайтами и мобильными приложениями
  • -
  • Разработка кастомизированных отчетов и дашбордов
  • -
  • Оптимизация производительности баз данных
  • -
-
-
-
- -
-

📞 Контакты

-
-
-
-

📧 Электронная почта

-

{{ CONTACT_EMAIL }}

-
-
-

📱 Телефон

-

{{ CONTACT_PHONE }}

-
-
-

💬 Telegram

-

@odinesina_prog

-
-
-
-
- - -
- - - +{% extends 'programmer/base.html' %} +{% load django_bootstrap5 %} + +{% block content %} + + +
+
+

Николай Сердюк

+

Разработчик 1С

+
+ +
+

🚀 Опыт работы

+

Более 10 лет успешной работы в разработке и сопровождении систем на платформе 1С

+ +
+

Основные направления:

+
+
+

💻 Разработка

+
    +
  • Разработка и доработка конфигураций 1С
  • +
  • Создание внешних обработок и отчетов
  • +
  • Кастомизация под бизнес-процессы
  • +
+
+
+

🔗 Интеграция

+
    +
  • Интеграция 1С с веб-сервисами
  • +
  • Связь с сайтами и мобильными приложениями
  • +
  • API и веб-сервисы
  • +
+
+
+

⚡ Оптимизация

+
    +
  • Оптимизация бизнес-процессов
  • +
  • Ускорение работы баз данных
  • +
  • Автоматизация рутинных операций
  • +
+
+
+
+
+ +
+

🛠 Технологии и навыки

+
+
+

🎯 1С Разработка

+
    +
  • 1С:Предприятие 8.3
  • +
  • Управление торговлей
  • +
  • Бухгалтерия предприятия
  • +
  • Зарплата и управление персоналом
  • +
  • Внешние обработки и отчеты
  • +
+
+
+

🔧 Дополнительные технологии

+
    +
  • SQL и оптимизация запросов
  • +
  • Веб-сервисы и API
  • +
  • XML, JSON, REST
  • +
  • Системное администрирование
  • +
+
+
+
+ +
+

📈 Проекты и достижения

+

Успешно реализовал более 50 проектов различной сложности

+ +
+
+

🏆 Ключевые проекты

+
    +
  • Автоматизация учетных систем для предприятий
  • +
  • Интеграция 1С с сайтами и мобильными приложениями
  • +
  • Разработка кастомизированных отчетов и дашбордов
  • +
  • Оптимизация производительности баз данных
  • +
+
+
+
+ +
+

📞 Контакты

+
+
+
+

📧 Электронная почта

+

{{ CONTACT_EMAIL }}

+
+
+

📱 Телефон

+

{{ CONTACT_PHONE }}

+
+
+

💬 Telegram

+

@odinesina_prog

+
+
+
+
+ + +
+ + + {% endblock %} \ No newline at end of file diff --git a/OneCprogsite/programmer/templates/programmer/base.html b/OneCprogsite/programmer/templates/programmer/base.html index ff10cda..937b093 100644 --- a/OneCprogsite/programmer/templates/programmer/base.html +++ b/OneCprogsite/programmer/templates/programmer/base.html @@ -1,578 +1,578 @@ -{% load static %} -{% load programmer_tags %} -{% load django_bootstrap5 %} - - - - {{title}} - - - - - - - - - - - - - - - - - - - - - - - {% bootstrap_css %} - - - - - - - - - - - - {% block extra_css %} - - {% endblock %} - - - - - - - - - - - - - - - {% block mainmenu %} -
-
- -
-
- - -
-
-
-

Меню

- -
- - - -
- - - Telegram - - -
- - -
-
-
- {% endblock mainmenu %} - - -
-
-
- - {% block breadcrumbs %} - - {% endblock %} - - - {% bootstrap_messages %} - - -
- {% block content %} - {% endblock %} -
-
-
-
- - -
-
- -
-
- - {% bootstrap_javascript %} - - - - {% block extra_js %} - - {% endblock %} - - - - +{% load static %} +{% load programmer_tags %} +{% load django_bootstrap5 %} + + + + {{title}} + + + + + + + + + + + + + + + + + + + + + + + {% bootstrap_css %} + + + + + + + + + + + + {% block extra_css %} + + {% endblock %} + + + + + + + + + + + + + + + {% block mainmenu %} +
+
+ +
+
+ + +
+
+
+

Меню

+ +
+ + + +
+ + + Telegram + + +
+ + +
+
+
+ {% endblock mainmenu %} + + +
+
+
+ + {% block breadcrumbs %} + + {% endblock %} + + + {% bootstrap_messages %} + + +
+ {% block content %} + {% endblock %} +
+
+
+
+ + +
+
+ +
+
+ + {% bootstrap_javascript %} + + + + {% block extra_js %} + + {% endblock %} + + + + \ No newline at end of file diff --git a/OneCprogsite/programmer/templates/programmer/callback.html b/OneCprogsite/programmer/templates/programmer/callback.html index 9106615..b5d665d 100644 --- a/OneCprogsite/programmer/templates/programmer/callback.html +++ b/OneCprogsite/programmer/templates/programmer/callback.html @@ -1,8 +1,8 @@ -{% extends 'programmer/base.html' %} -{% load django_bootstrap5 %} - -{% block content %} - - - +{% extends 'programmer/base.html' %} +{% load django_bootstrap5 %} + +{% block content %} + + + {% endblock %} \ No newline at end of file diff --git a/OneCprogsite/programmer/templates/programmer/competence.html b/OneCprogsite/programmer/templates/programmer/competence.html index 2c261b5..b425251 100644 --- a/OneCprogsite/programmer/templates/programmer/competence.html +++ b/OneCprogsite/programmer/templates/programmer/competence.html @@ -1,72 +1,72 @@ -{% extends 'programmer/base.html' %} -{% load django_bootstrap5 %} -{% load static %} - -{% block extra_css %} - -{% endblock %} - -{% block content %} - - -
- {% for p in posts %} -
-
- {% if p.photo %} -
-
- Сертификат 1С: {{ p.title }} - {{ p.content|striptags }} -
- 🔍 - Нажмите для увеличения -
-
-
- {% endif %} - -
-

{{ p.title }}

-
- {{ p.content|linebreaks }} -
-
-
-
- {% endfor %} -
- -{% if not posts %} -
-

📚 Информация о компетенциях

-

Раздел находится в разработке

- -
-{% endif %} - - - -{% endblock %} - -{% block extra_js %} - +{% extends 'programmer/base.html' %} +{% load django_bootstrap5 %} +{% load static %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} + + +
+ {% for p in posts %} +
+
+ {% if p.photo %} +
+
+ Сертификат 1С: {{ p.title }} - {{ p.content|striptags }} +
+ 🔍 + Нажмите для увеличения +
+
+
+ {% endif %} + +
+

{{ p.title }}

+
+ {{ p.content|linebreaks }} +
+
+
+
+ {% endfor %} +
+ +{% if not posts %} +
+

📚 Информация о компетенциях

+

Раздел находится в разработке

+ +
+{% endif %} + + + +{% endblock %} + +{% block extra_js %} + {% endblock %} \ No newline at end of file diff --git a/OneCprogsite/programmer/templates/programmer/humans.txt b/OneCprogsite/programmer/templates/programmer/humans.txt index 7a8f4bc..b4cb295 100644 --- a/OneCprogsite/programmer/templates/programmer/humans.txt +++ b/OneCprogsite/programmer/templates/programmer/humans.txt @@ -1,13 +1,13 @@ -/* TEAM */ -Developer: Николай Сердюк -Site: https://nikdizell.ru -Email: {{ CONTACT_EMAIL }} - -/* THANKS */ -Django Framework -Bootstrap - -/* SITE */ -Last update: 2025 -Language: Russian +/* TEAM */ +Developer: Николай Сердюк +Site: https://nikdizell.ru +Email: {{ CONTACT_EMAIL }} + +/* THANKS */ +Django Framework +Bootstrap + +/* SITE */ +Last update: 2025 +Language: Russian Doctype: HTML5 \ No newline at end of file diff --git a/OneCprogsite/programmer/templates/programmer/index.html b/OneCprogsite/programmer/templates/programmer/index.html index ca59bd6..ff5aa19 100644 --- a/OneCprogsite/programmer/templates/programmer/index.html +++ b/OneCprogsite/programmer/templates/programmer/index.html @@ -1,103 +1,103 @@ -{% extends 'programmer/base.html' %} -{% load django_bootstrap5 %} - -{% block content %} -
-

🚀 Добро пожаловать!

-

Я профессиональный программист 1С с опытом создания эффективных бизнес-решений

-
- -
- {% autoescape off %} - {% for p in posts %} -
-
-

{{p.title}}

-
-
- {{p.content}} -
-
- -
-
- {% endfor %} - {% endautoescape %} -
- - - - -{% if not posts %} -
-

🚀 Контент скоро появится

-

Мы готовим для вас интересные материалы и кейсы

-
- -
-
-{% endif %} - - +{% extends 'programmer/base.html' %} +{% load django_bootstrap5 %} + +{% block content %} +
+

🚀 Добро пожаловать!

+

Я профессиональный программист 1С с опытом создания эффективных бизнес-решений

+
+ +
+ {% autoescape off %} + {% for p in posts %} +
+
+

{{p.title}}

+
+
+ {{p.content}} +
+
+ +
+
+ {% endfor %} + {% endautoescape %} +
+ + + + +{% if not posts %} +
+

🚀 Контент скоро появится

+

Мы готовим для вас интересные материалы и кейсы

+
+ +
+
+{% endif %} + + {% endblock %} \ No newline at end of file diff --git a/OneCprogsite/programmer/templates/programmer/recall.html b/OneCprogsite/programmer/templates/programmer/recall.html index faca463..c1bd193 100644 --- a/OneCprogsite/programmer/templates/programmer/recall.html +++ b/OneCprogsite/programmer/templates/programmer/recall.html @@ -1,174 +1,174 @@ -{% extends 'programmer/base.html' %} -{% load django_bootstrap5 %} -{% load static %} -{% load seo_tags %} - -{% block extra_css %} - -{% endblock %} - -{% block content %} - - -
- {% for p in posts %} -
- - - - -
-
-
-

{{ p.title }}

- {% if p.time_create %} - - - - {% endif %} -
-
- -
- {% if p.scan %} -
-
- Отзыв от {{ p.title }} -
- 🔍 - Нажмите для увеличения -
-
-
- {% endif %} - -
- {{ p.content|linebreaks }} -
-
-
-
- {% endfor %} -
- -{% if not posts %} -
-

💬 Отзывы клиентов

-

Здесь будут отображаться отзывы от довольных клиентов

- -
-{% endif %} - - - - - +{% extends 'programmer/base.html' %} +{% load django_bootstrap5 %} +{% load static %} +{% load seo_tags %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} + + +
+ {% for p in posts %} +
+ + + + +
+
+
+

{{ p.title }}

+ {% if p.time_create %} + + + + {% endif %} +
+
+ +
+ {% if p.scan %} +
+
+ Отзыв от {{ p.title }} +
+ 🔍 + Нажмите для увеличения +
+
+
+ {% endif %} + +
+ {{ p.content|linebreaks }} +
+
+
+
+ {% endfor %} +
+ +{% if not posts %} +
+

💬 Отзывы клиентов

+

Здесь будут отображаться отзывы от довольных клиентов

+ +
+{% endif %} + + + + + {% endblock %} \ No newline at end of file diff --git a/OneCprogsite/programmer/templates/programmer/robots.txt b/OneCprogsite/programmer/templates/programmer/robots.txt index 32d69b7..a3197a0 100644 --- a/OneCprogsite/programmer/templates/programmer/robots.txt +++ b/OneCprogsite/programmer/templates/programmer/robots.txt @@ -1,19 +1,19 @@ -User-agent: * -Allow: / - -# Основной сайт -Sitemap: {{ request.scheme }}://{{ request.get_host }}/sitemap.xml - -# Запрещаем служебные разделы -Disallow: /admin/ -Disallow: /media/cache/ -Disallow: /static/admin/ -Disallow: /callback/ -Disallow: /api/ - -# Разрешаем индексацию статических файлов -Allow: /static/ -Allow: /media/ - -# Указываем главное зеркало +User-agent: * +Allow: / + +# Основной сайт +Sitemap: {{ request.scheme }}://{{ request.get_host }}/sitemap.xml + +# Запрещаем служебные разделы +Disallow: /admin/ +Disallow: /media/cache/ +Disallow: /static/admin/ +Disallow: /callback/ +Disallow: /api/ + +# Разрешаем индексацию статических файлов +Allow: /static/ +Allow: /media/ + +# Указываем главное зеркало Host: {{ request.scheme }}://{{ request.get_host }} \ No newline at end of file diff --git a/OneCprogsite/programmer/templates/programmer/sitemap.xml b/OneCprogsite/programmer/templates/programmer/sitemap.xml index 62e1b06..59012bd 100644 --- a/OneCprogsite/programmer/templates/programmer/sitemap.xml +++ b/OneCprogsite/programmer/templates/programmer/sitemap.xml @@ -1,12 +1,12 @@ - - -{% for url in urlset %} - - {{ url.location }} - {% if url.lastmod %}{{ url.lastmod|date:"Y-m-d" }}{% endif %} - {% if url.changefreq %}{{ url.changefreq }}{% endif %} - {% if url.priority %}{{ url.priority }}{% endif %} - -{% endfor %} + + +{% for url in urlset %} + + {{ url.location }} + {% if url.lastmod %}{{ url.lastmod|date:"Y-m-d" }}{% endif %} + {% if url.changefreq %}{{ url.changefreq }}{% endif %} + {% if url.priority %}{{ url.priority }}{% endif %} + +{% endfor %} \ No newline at end of file diff --git a/OneCprogsite/programmer/templates/programmer/solution.html b/OneCprogsite/programmer/templates/programmer/solution.html index babc513..6a9e42f 100644 --- a/OneCprogsite/programmer/templates/programmer/solution.html +++ b/OneCprogsite/programmer/templates/programmer/solution.html @@ -1,96 +1,96 @@ -{% extends 'programmer/base.html' %} -{% load django_bootstrap5 %} -{% load static %} -{% load seo_tags %} - -{% block extra_css %} - -{% endblock %} - -{% block content %} - - -
- {% autoescape off %} - {% for p in posts %} -
  • -
    -

    {{p.title}}

    - - - - -
    -
    -
    - 📋 Описание задачи - -
    -
    - {{p.description}} -
    -
    - -
    -
    - 🔧 Описание решения - -
    -
    - {{p.implementation}} -
    -
    - -
    -
    - ✅ Результат - -
    -
    - {{p.closing}} -
    -
    -
    - -
    -

    Опубликовано: {{p.time_create|date:"d.m.Y"}}

    -
    -
    - - {% empty %} -
    -

    🚀 Проекты в разработке

    -

    Скоро здесь появятся новые кейсы автоматизации

    -
    - -
  • - {% endfor %} - {% endautoescape %} -
    - -{% if not posts %} -
    -

    Примеры решений скоро появятся

    -

    Мы готовим для вас интересные кейсы и решения

    -
    -{% endif %} - - - - +{% extends 'programmer/base.html' %} +{% load django_bootstrap5 %} +{% load static %} +{% load seo_tags %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} + + +
    + {% autoescape off %} + {% for p in posts %} +
  • +
    +

    {{p.title}}

    + + + + +
    +
    +
    + 📋 Описание задачи + +
    +
    + {{p.description}} +
    +
    + +
    +
    + 🔧 Описание решения + +
    +
    + {{p.implementation}} +
    +
    + +
    +
    + ✅ Результат + +
    +
    + {{p.closing}} +
    +
    +
    + +
    +

    Опубликовано: {{p.time_create|date:"d.m.Y"}}

    +
    +
    + + {% empty %} +
    +

    🚀 Проекты в разработке

    +

    Скоро здесь появятся новые кейсы автоматизации

    +
    + +
  • + {% endfor %} + {% endautoescape %} +
    + +{% if not posts %} +
    +

    Примеры решений скоро появятся

    +

    Мы готовим для вас интересные кейсы и решения

    +
    +{% endif %} + + + + {% endblock %} \ No newline at end of file diff --git a/OneCprogsite/programmer/templatetags/programmer_tags.py b/OneCprogsite/programmer/templatetags/programmer_tags.py index 5ca4387..cbb735b 100644 --- a/OneCprogsite/programmer/templatetags/programmer_tags.py +++ b/OneCprogsite/programmer/templatetags/programmer_tags.py @@ -1,14 +1,14 @@ -from django import template -from ..models import CallbackRequest - -register = template.Library() - -@register.simple_tag -def get_unread_callbacks(): - return CallbackRequest.objects.filter(is_read=False).count() - -@register.simple_tag -def get_today_callbacks(): - from django.utils import timezone - today = timezone.now().date() +from django import template +from ..models import CallbackRequest + +register = template.Library() + +@register.simple_tag +def get_unread_callbacks(): + return CallbackRequest.objects.filter(is_read=False).count() + +@register.simple_tag +def get_today_callbacks(): + from django.utils import timezone + today = timezone.now().date() return CallbackRequest.objects.filter(time_create__date=today).count() \ No newline at end of file diff --git a/OneCprogsite/programmer/templatetags/seo_tags.py b/OneCprogsite/programmer/templatetags/seo_tags.py index 0022519..dcbf0a9 100644 --- a/OneCprogsite/programmer/templatetags/seo_tags.py +++ b/OneCprogsite/programmer/templatetags/seo_tags.py @@ -1,21 +1,21 @@ -from django import template -from django.utils.html import strip_tags - -register = template.Library() - -@register.simple_tag -def generate_meta_description(obj, default=""): - """Генерирует meta description для объектов""" - if hasattr(obj, 'get_seo_description'): - return obj.get_seo_description() - elif hasattr(obj, 'content'): - clean_content = strip_tags(obj.content)[:160] - return clean_content + '...' if len(clean_content) > 160 else clean_content - return default - -@register.simple_tag -def generate_meta_keywords(obj, default=""): - """Генерирует meta keywords для объектов""" - if hasattr(obj, 'get_meta_keywords'): - return ', '.join(obj.get_meta_keywords()) +from django import template +from django.utils.html import strip_tags + +register = template.Library() + +@register.simple_tag +def generate_meta_description(obj, default=""): + """Генерирует meta description для объектов""" + if hasattr(obj, 'get_seo_description'): + return obj.get_seo_description() + elif hasattr(obj, 'content'): + clean_content = strip_tags(obj.content)[:160] + return clean_content + '...' if len(clean_content) > 160 else clean_content + return default + +@register.simple_tag +def generate_meta_keywords(obj, default=""): + """Генерирует meta keywords для объектов""" + if hasattr(obj, 'get_meta_keywords'): + return ', '.join(obj.get_meta_keywords()) return default \ No newline at end of file diff --git a/OneCprogsite/programmer/tests.py b/OneCprogsite/programmer/tests.py index 7ce503c..de8bdc0 100644 --- a/OneCprogsite/programmer/tests.py +++ b/OneCprogsite/programmer/tests.py @@ -1,3 +1,3 @@ -from django.test import TestCase - -# Create your tests here. +from django.test import TestCase + +# Create your tests here. diff --git a/OneCprogsite/programmer/urls.py b/OneCprogsite/programmer/urls.py index ed69c18..2c961c3 100644 --- a/OneCprogsite/programmer/urls.py +++ b/OneCprogsite/programmer/urls.py @@ -1,27 +1,27 @@ -from django.contrib import admin -from django.urls import path, include -from django.conf import settings -from django.conf.urls.static import static -from .views import * -from django.contrib.sitemaps.views import sitemap -from .sitemaps import sitemaps - - -urlpatterns = [ - path('', index, name='home'), - path('about/', about, name='about'), - path('solutions/', solution, name='solution'), - path('competence/', ability, name='ability'), - path('recall/', recall, name='recall'), - path('post//', show_post, name='post'), - path('callback/', callback_request, name='callback'), - path('admin/statistics/', statistics_view, name='statistics'), - # Sitemap - path('sitemap.xml', sitemap, {'sitemaps': sitemaps}, - name='django.contrib.sitemaps.views.sitemap'), - path('robots.txt', robots_txt, name='robots'), -] - - -if settings.DEBUG: +from django.contrib import admin +from django.urls import path, include +from django.conf import settings +from django.conf.urls.static import static +from .views import * +from django.contrib.sitemaps.views import sitemap +from .sitemaps import sitemaps + + +urlpatterns = [ + path('', index, name='home'), + path('about/', about, name='about'), + path('solutions/', solution, name='solution'), + path('competence/', ability, name='ability'), + path('recall/', recall, name='recall'), + path('post//', show_post, name='post'), + path('callback/', callback_request, name='callback'), + path('admin/statistics/', statistics_view, name='statistics'), + # Sitemap + path('sitemap.xml', sitemap, {'sitemaps': sitemaps}, + name='django.contrib.sitemaps.views.sitemap'), + path('robots.txt', robots_txt, name='robots'), +] + + +if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \ No newline at end of file diff --git a/OneCprogsite/programmer/utils/email_notifications.py b/OneCprogsite/programmer/utils/email_notifications.py index fa902a8..ffbe2ad 100644 --- a/OneCprogsite/programmer/utils/email_notifications.py +++ b/OneCprogsite/programmer/utils/email_notifications.py @@ -1,65 +1,65 @@ -# programmer/utils/email_notifications.py -from django.core.mail import send_mail, EmailMultiAlternatives -from django.template.loader import render_to_string -from django.conf import settings -from django.utils.html import strip_tags -import logging - -logger = logging.getLogger(__name__) - - -def send_callback_notification(callback_request): - """ - Отправляет уведомление о новой заявке на обратный звонок - """ - try: - subject = f'🚨 Новая заявка на обратный звонок от {callback_request.name}' - - # HTML версия письма - html_message = render_to_string('emails/callback_notification.html', { - 'callback': callback_request, - 'site_url': settings.ALLOWED_HOSTS[0] if settings.ALLOWED_HOSTS else 'localhost', - }) - - # Текстовая версия письма - plain_message = strip_tags(html_message) - - # Проверяем настройки email - if not all([settings.EMAIL_HOST_USER, settings.EMAIL_HOST_PASSWORD]): - logger.error("Email settings are not configured properly") - return False - - # Отправляем email - send_mail( - subject=subject, - message=plain_message, - from_email=settings.DEFAULT_FROM_EMAIL, - recipient_list=settings.ADMIN_EMAILS, - html_message=html_message, - fail_silently=False, - ) - - logger.info(f"Email notification sent successfully for callback #{callback_request.id}") - return True - - except Exception as e: - logger.error(f"Error sending email notification: {e}") - return False - - -def send_test_email(): - """ - Функция для тестирования отправки email - """ - try: - send_mail( - subject='📧 Test Email from Django', - message='This is a test email from your Django application.', - from_email=settings.DEFAULT_FROM_EMAIL, - recipient_list=settings.ADMIN_EMAILS, - fail_silently=False, - ) - return True - except Exception as e: - logger.error(f"Test email failed: {e}") - return False +# programmer/utils/email_notifications.py +from django.core.mail import send_mail, EmailMultiAlternatives +from django.template.loader import render_to_string +from django.conf import settings +from django.utils.html import strip_tags +import logging + +logger = logging.getLogger(__name__) + + +def send_callback_notification(callback_request): + """ + Отправляет уведомление о новой заявке на обратный звонок + """ + try: + subject = f'🚨 Новая заявка на обратный звонок от {callback_request.name}' + + # HTML версия письма + html_message = render_to_string('emails/callback_notification.html', { + 'callback': callback_request, + 'site_url': settings.ALLOWED_HOSTS[0] if settings.ALLOWED_HOSTS else 'localhost', + }) + + # Текстовая версия письма + plain_message = strip_tags(html_message) + + # Проверяем настройки email + if not all([settings.EMAIL_HOST_USER, settings.EMAIL_HOST_PASSWORD]): + logger.error("Email settings are not configured properly") + return False + + # Отправляем email + send_mail( + subject=subject, + message=plain_message, + from_email=settings.DEFAULT_FROM_EMAIL, + recipient_list=settings.ADMIN_EMAILS, + html_message=html_message, + fail_silently=False, + ) + + logger.info(f"Email notification sent successfully for callback #{callback_request.id}") + return True + + except Exception as e: + logger.error(f"Error sending email notification: {e}") + return False + + +def send_test_email(): + """ + Функция для тестирования отправки email + """ + try: + send_mail( + subject='📧 Test Email from Django', + message='This is a test email from your Django application.', + from_email=settings.DEFAULT_FROM_EMAIL, + recipient_list=settings.ADMIN_EMAILS, + fail_silently=False, + ) + return True + except Exception as e: + logger.error(f"Test email failed: {e}") + return False diff --git a/OneCprogsite/programmer/views.py b/OneCprogsite/programmer/views.py index 999b213..a4a09ae 100644 --- a/OneCprogsite/programmer/views.py +++ b/OneCprogsite/programmer/views.py @@ -1,259 +1,259 @@ -from django.http import HttpResponse, HttpResponseNotFound -from .models import * -from django.shortcuts import render, redirect -from django.contrib import messages -from .models import CallbackRequest # Импортируем из models, а не forms -from .forms import CallbackForm -from django.utils import timezone -from datetime import timedelta -from .models import PageView, Visitor -from django.db.models import Count -from django.contrib.auth.decorators import login_required, user_passes_test -from django.views.decorators.http import require_GET - - -menu = [ - {'title': "Главная", 'url_name': 'home'}, - {'title': "Проекты", 'url_name': 'solution'}, - {'title': "Компетенции", 'url_name': 'ability'}, - {'title': "Отзывы", 'url_name': 'recall'}, - {'title': "Обо мне", 'url_name': 'about'} - ] - - -# === ДОБАВЬТЕ ЭТИ ФУНКЦИИ ЗДЕСЬ === - -def get_client_ip(request): - """Получаем реальный IP клиента""" - x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') - if x_forwarded_for: - ip = x_forwarded_for.split(',')[0] - else: - ip = request.META.get('REMOTE_ADDR') - return ip - - -def should_track_request(request): - """Определяем, нужно ли отслеживать запрос""" - - client_ip = get_client_ip(request) - path = request.path - - # Игнорируемые пути (Nextcloud специфичные) - nextcloud_paths = [ - '/index.php', - '/status.php', - '/cron', - '/remote.php', - '/ocs', - '/apps/', - '/custom_apps/', - ] - - # Игнорируемые IP (Docker сети) - docker_ips = [ - '192.168.64.1', - '192.168.65.1', - '172.17.0.1', - '172.18.0.1', - '172.19.0.1', - ] - - # Игнорируем статические файлы и админку - if path.startswith('/static/') or path.startswith('/admin/'): - return False - - # Не отслеживаем Nextcloud и Docker запросы - if any(path.startswith(p) for p in nextcloud_paths): - return False - if client_ip in docker_ips: - return False - - return True - - -def track_page_view(request): - """Основная функция отслеживания просмотров""" - if not should_track_request(request): - return - - try: - PageView.objects.create( - url=request.path, - ip_address=get_client_ip(request), - user_agent=request.META.get('HTTP_USER_AGENT', '')[:500], - referer=request.META.get('HTTP_REFERER', '')[:500], - ) - except Exception as e: - print(f"Error tracking page: {e}") - - -def track_view(view_func): - """Декоратор для отслеживания просмотров страниц""" - from functools import wraps - - @wraps(view_func) - def _wrapped_view(request, *args, **kwargs): - # Отслеживаем просмотр перед выполнением view - track_page_view(request) - return view_func(request, *args, **kwargs) - - return _wrapped_view - - -@track_view -def index(request): - posts = Home.objects.filter(is_published=True) - context = { - 'posts': posts, - 'menu': menu, - 'title': "Программист 1С Николай Сердюк - разработка и сопровождение", - 'meta_description': "Профессиональный программист 1С с более чем 10-летним опытом. Разработка, доработка, обновление и интеграция систем 1С. Сопровождение 1С.", - 'meta_keywords': "программист 1С, разработка 1С, обновление 1С, сопровождение 1С, интеграция 1С, доработка 1С, 1С предприятие 8.3", - 'form': CallbackForm() - } - return render(request, 'programmer/index.html', context=context) - - -@track_view -def about(request): - context = { - 'menu': menu, - 'title': "Программист 1С Николай Сердюк - 10+ лет опыта | Услуги 1С", - 'meta_description': "Николай Сердюк - сертифицированный программист 1С с 10+ лет опыта. Специализация: обновление 1С, разработка под ключ, интеграция, миграция с 1С 7.7.", - 'meta_keywords': "программист 1С Николай Сердюк, обновление 1С, разработка 1С под ключ, интеграция 1С, сертифицированный 1С, миграция 1С 7.7" - } - return render(request, 'programmer/about.html', context=context) - - -@track_view -def solution(request): - posts = Solution.objects.filter(is_published=True) - context = { - 'posts': posts, - 'menu': menu, - 'meta_description': "Реализованные проекты по автоматизации 1С: складской учет с ТСД, интеграция с оборудованием, миграция с 1С 7.7. Примеры работ и кейсы.", - 'meta_keywords': "проекты 1С, автоматизация склада 1С, интеграция ТСД 1С, миграция 1С 7.7, кейсы 1С, примеры работ 1С", - 'title': "Проекты автоматизации 1С | Реализованные кейсы и решения", - } - return render(request, 'programmer/solution.html', context=context) - - -@track_view -def ability(request): - posts = Competence.objects.filter(is_published=True) - context = { - 'posts': posts, - 'menu': menu, - 'title': "Сертификаты и компетенции 1С | Программист 1С Николай Сердюк", - 'meta_description': "Сертификаты 1С: Профессионал по платформе 8.3 и БП 3.0. Подтвержденная квалификация программиста 1С с сертификатами фирмы 1С.", - 'meta_keywords': "сертификаты 1С, 1С профессионал, компетенции 1С, квалификация программиста 1С, сертифицированный специалист 1С" - } - return render(request, 'programmer/competence.html', context=context) - - -@track_view -def recall(request): - posts = Recall.objects.filter(is_published=True) - context = { - 'posts': posts, - 'menu': menu, - 'title': "Отзывы клиентов о работе программиста 1С | Реальные кейсы", - 'meta_description': "Реальные отзывы клиентов о работе программиста 1С Николая Сердюка. Отзывы от ООО «РОВЕН-Регионы» и других компаний.", - 'meta_keywords': "отзывы программист 1С, рекомендации 1С, отзывы клиентов 1С, реальные кейсы 1С, отзыв ООО РОВЕН" - } - return render(request, 'programmer/recall.html', context=context) - - -def show_post(request, post_id): - return HttpResponse(f"Отображение № {post_id}") - - -def pageNotFound(request, exception): - return HttpResponseNotFound('

    Страница не найдена

    ') - - -def callback_request(request): - if request.method == 'POST': - form = CallbackForm(request.POST) - if form.is_valid(): - # Сохраняем заявку через форму - form.save() - messages.success(request, '✅ Ваша заявка успешно отправлена! Я свяжусь с вами в ближайшее время.') - return redirect('home') - else: - # Если форма невалидна, показываем ошибки - for field, errors in form.errors.items(): - for error in errors: - messages.error(request, f'❌ Ошибка в поле {form.fields[field].label}: {error}') - return redirect('home') - - # Если GET запрос, просто показываем главную страницу - return redirect('home') - - -def is_admin(user): - return user.is_staff - - -def is_staff(user): - return user.is_staff - - -@login_required -@user_passes_test(is_staff) -def statistics_view(request): - today = timezone.now().date() - week_ago = today - timedelta(days=7) - - # Статистика за сегодня - today_views = PageView.objects.filter( - timestamp__date=today - ).count() - - # Статистика за неделю - weekly_views = PageView.objects.filter( - timestamp__date__gte=week_ago - ).count() - - # Всего просмотров - total_views = PageView.objects.count() - - # Популярные страницы за неделю - popular_pages = PageView.objects.filter( - timestamp__date__gte=week_ago - ).values('url').annotate( - views=Count('id') - ).order_by('-views')[:10] - - # Уникальные посетители за неделю - unique_visitors = Visitor.objects.filter( - last_visit__date__gte=week_ago - ).count() - - # Последние посещения - recent_views = PageView.objects.select_related().order_by('-timestamp')[:20] - - today = timezone.now().date() - total_callbacks = CallbackRequest.objects.count() - today_callbacks = CallbackRequest.objects.filter(time_create__date=today).count() - unread_callbacks = CallbackRequest.objects.filter(is_read=False).count() - - context = { - 'today_views': today_views, - 'weekly_views': weekly_views, - 'total_views': total_views, - 'unique_visitors': unique_visitors, - 'popular_pages': popular_pages, - 'recent_views': recent_views, - 'total_callbacks': total_callbacks, - 'today_callbacks': today_callbacks, - 'unread_callbacks': unread_callbacks, - } - - return render(request, 'admin/statistics.html', context) - - -@require_GET -def robots_txt(request): - return render(request, 'robots.txt', content_type='text/plain') +from django.http import HttpResponse, HttpResponseNotFound +from .models import * +from django.shortcuts import render, redirect +from django.contrib import messages +from .models import CallbackRequest # Импортируем из models, а не forms +from .forms import CallbackForm +from django.utils import timezone +from datetime import timedelta +from .models import PageView, Visitor +from django.db.models import Count +from django.contrib.auth.decorators import login_required, user_passes_test +from django.views.decorators.http import require_GET + + +menu = [ + {'title': "Главная", 'url_name': 'home'}, + {'title': "Проекты", 'url_name': 'solution'}, + {'title': "Компетенции", 'url_name': 'ability'}, + {'title': "Отзывы", 'url_name': 'recall'}, + {'title': "Обо мне", 'url_name': 'about'} + ] + + +# === ДОБАВЬТЕ ЭТИ ФУНКЦИИ ЗДЕСЬ === + +def get_client_ip(request): + """Получаем реальный IP клиента""" + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + if x_forwarded_for: + ip = x_forwarded_for.split(',')[0] + else: + ip = request.META.get('REMOTE_ADDR') + return ip + + +def should_track_request(request): + """Определяем, нужно ли отслеживать запрос""" + + client_ip = get_client_ip(request) + path = request.path + + # Игнорируемые пути (Nextcloud специфичные) + nextcloud_paths = [ + '/index.php', + '/status.php', + '/cron', + '/remote.php', + '/ocs', + '/apps/', + '/custom_apps/', + ] + + # Игнорируемые IP (Docker сети) + docker_ips = [ + '192.168.64.1', + '192.168.65.1', + '172.17.0.1', + '172.18.0.1', + '172.19.0.1', + ] + + # Игнорируем статические файлы и админку + if path.startswith('/static/') or path.startswith('/admin/'): + return False + + # Не отслеживаем Nextcloud и Docker запросы + if any(path.startswith(p) for p in nextcloud_paths): + return False + if client_ip in docker_ips: + return False + + return True + + +def track_page_view(request): + """Основная функция отслеживания просмотров""" + if not should_track_request(request): + return + + try: + PageView.objects.create( + url=request.path, + ip_address=get_client_ip(request), + user_agent=request.META.get('HTTP_USER_AGENT', '')[:500], + referer=request.META.get('HTTP_REFERER', '')[:500], + ) + except Exception as e: + print(f"Error tracking page: {e}") + + +def track_view(view_func): + """Декоратор для отслеживания просмотров страниц""" + from functools import wraps + + @wraps(view_func) + def _wrapped_view(request, *args, **kwargs): + # Отслеживаем просмотр перед выполнением view + track_page_view(request) + return view_func(request, *args, **kwargs) + + return _wrapped_view + + +@track_view +def index(request): + posts = Home.objects.filter(is_published=True) + context = { + 'posts': posts, + 'menu': menu, + 'title': "Программист 1С Николай Сердюк - разработка и сопровождение", + 'meta_description': "Профессиональный программист 1С с более чем 10-летним опытом. Разработка, доработка, обновление и интеграция систем 1С. Сопровождение 1С.", + 'meta_keywords': "программист 1С, разработка 1С, обновление 1С, сопровождение 1С, интеграция 1С, доработка 1С, 1С предприятие 8.3", + 'form': CallbackForm() + } + return render(request, 'programmer/index.html', context=context) + + +@track_view +def about(request): + context = { + 'menu': menu, + 'title': "Программист 1С Николай Сердюк - 10+ лет опыта | Услуги 1С", + 'meta_description': "Николай Сердюк - сертифицированный программист 1С с 10+ лет опыта. Специализация: обновление 1С, разработка под ключ, интеграция, миграция с 1С 7.7.", + 'meta_keywords': "программист 1С Николай Сердюк, обновление 1С, разработка 1С под ключ, интеграция 1С, сертифицированный 1С, миграция 1С 7.7" + } + return render(request, 'programmer/about.html', context=context) + + +@track_view +def solution(request): + posts = Solution.objects.filter(is_published=True) + context = { + 'posts': posts, + 'menu': menu, + 'meta_description': "Реализованные проекты по автоматизации 1С: складской учет с ТСД, интеграция с оборудованием, миграция с 1С 7.7. Примеры работ и кейсы.", + 'meta_keywords': "проекты 1С, автоматизация склада 1С, интеграция ТСД 1С, миграция 1С 7.7, кейсы 1С, примеры работ 1С", + 'title': "Проекты автоматизации 1С | Реализованные кейсы и решения", + } + return render(request, 'programmer/solution.html', context=context) + + +@track_view +def ability(request): + posts = Competence.objects.filter(is_published=True) + context = { + 'posts': posts, + 'menu': menu, + 'title': "Сертификаты и компетенции 1С | Программист 1С Николай Сердюк", + 'meta_description': "Сертификаты 1С: Профессионал по платформе 8.3 и БП 3.0. Подтвержденная квалификация программиста 1С с сертификатами фирмы 1С.", + 'meta_keywords': "сертификаты 1С, 1С профессионал, компетенции 1С, квалификация программиста 1С, сертифицированный специалист 1С" + } + return render(request, 'programmer/competence.html', context=context) + + +@track_view +def recall(request): + posts = Recall.objects.filter(is_published=True) + context = { + 'posts': posts, + 'menu': menu, + 'title': "Отзывы клиентов о работе программиста 1С | Реальные кейсы", + 'meta_description': "Реальные отзывы клиентов о работе программиста 1С Николая Сердюка. Отзывы от ООО «РОВЕН-Регионы» и других компаний.", + 'meta_keywords': "отзывы программист 1С, рекомендации 1С, отзывы клиентов 1С, реальные кейсы 1С, отзыв ООО РОВЕН" + } + return render(request, 'programmer/recall.html', context=context) + + +def show_post(request, post_id): + return HttpResponse(f"Отображение № {post_id}") + + +def pageNotFound(request, exception): + return HttpResponseNotFound('

    Страница не найдена

    ') + + +def callback_request(request): + if request.method == 'POST': + form = CallbackForm(request.POST) + if form.is_valid(): + # Сохраняем заявку через форму + form.save() + messages.success(request, '✅ Ваша заявка успешно отправлена! Я свяжусь с вами в ближайшее время.') + return redirect('home') + else: + # Если форма невалидна, показываем ошибки + for field, errors in form.errors.items(): + for error in errors: + messages.error(request, f'❌ Ошибка в поле {form.fields[field].label}: {error}') + return redirect('home') + + # Если GET запрос, просто показываем главную страницу + return redirect('home') + + +def is_admin(user): + return user.is_staff + + +def is_staff(user): + return user.is_staff + + +@login_required +@user_passes_test(is_staff) +def statistics_view(request): + today = timezone.now().date() + week_ago = today - timedelta(days=7) + + # Статистика за сегодня + today_views = PageView.objects.filter( + timestamp__date=today + ).count() + + # Статистика за неделю + weekly_views = PageView.objects.filter( + timestamp__date__gte=week_ago + ).count() + + # Всего просмотров + total_views = PageView.objects.count() + + # Популярные страницы за неделю + popular_pages = PageView.objects.filter( + timestamp__date__gte=week_ago + ).values('url').annotate( + views=Count('id') + ).order_by('-views')[:10] + + # Уникальные посетители за неделю + unique_visitors = Visitor.objects.filter( + last_visit__date__gte=week_ago + ).count() + + # Последние посещения + recent_views = PageView.objects.select_related().order_by('-timestamp')[:20] + + today = timezone.now().date() + total_callbacks = CallbackRequest.objects.count() + today_callbacks = CallbackRequest.objects.filter(time_create__date=today).count() + unread_callbacks = CallbackRequest.objects.filter(is_read=False).count() + + context = { + 'today_views': today_views, + 'weekly_views': weekly_views, + 'total_views': total_views, + 'unique_visitors': unique_visitors, + 'popular_pages': popular_pages, + 'recent_views': recent_views, + 'total_callbacks': total_callbacks, + 'today_callbacks': today_callbacks, + 'unread_callbacks': unread_callbacks, + } + + return render(request, 'admin/statistics.html', context) + + +@require_GET +def robots_txt(request): + return render(request, 'robots.txt', content_type='text/plain') diff --git a/OneCprogsite/settings.py b/OneCprogsite/settings.py index 640b35a..a02cb85 100644 --- a/OneCprogsite/settings.py +++ b/OneCprogsite/settings.py @@ -1,212 +1,212 @@ -""" -Django settings for OneCprogsite project. - -Generated by 'django-admin startproject' using Django 4.2.7. - -For more information on this file, see -https://docs.djangoproject.com/en/4.2/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/4.2/ref/settings/ -""" -import os.path -from pathlib import Path - -# Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent - - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'django-insecure-5rs2a1*8cxjkv*%6k1-88biv&1#nep%@i+%1^dk=5j$s&e&hwm' - -# Безопасность cookies для HTTPS -SESSION_COOKIE_SECURE = True -CSRF_COOKIE_SECURE = True -SESSION_COOKIE_HTTPONLY = True -CSRF_COOKIE_HTTPONLY = False # Django требует доступ к CSRF cookie через JS -SESSION_COOKIE_SAMESITE = 'Lax' -CSRF_COOKIE_SAMESITE = 'Lax' - -# Если используете другие cookies -LANGUAGE_COOKIE_SECURE = True -LANGUAGE_COOKIE_HTTPONLY = True -LANGUAGE_COOKIE_SAMESITE = 'Lax' - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = False - -X_FRAME_OPTIONS = 'SAMEORIGIN' -# Или разрешить конкретные домены (Django 4.0+) -X_FRAME_OPTIONS = 'ALLOW-FROM https://metrika.yandex.ru' - -# ОБЯЗАТЕЛЬНО укажите ваши домены -ALLOWED_HOSTS = [ - 'nikdizell.ru', - 'www.nikdizell.ru', - 'localhost', - '127.0.0.1', - '192.168.31.88' # Добавьте IP сервера -] - -# Важно для работы за прокси -SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') -SECURE_SSL_REDIRECT = True - -# Дополнительная безопасность -SECURE_BROWSER_XSS_FILTER = True -SECURE_CONTENT_TYPE_NOSNIFF = True -SECURE_HSTS_SECONDS = 31536000 -SECURE_HSTS_INCLUDE_SUBDOMAINS = True -SECURE_HSTS_PRELOAD = True - -CSRF_TRUSTED_ORIGINS = [ - 'https://nikdizell.ru', - 'https://www.nikdizell.ru', -] - -# Application definition - -INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'programmer.apps.ProgrammerConfig', - 'django_bootstrap5', - 'django_extensions', - 'django.contrib.sites', - 'django.contrib.sitemaps', -] - -MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'programmer.middleware.PageViewMiddleware', -] - -ROOT_URLCONF = 'OneCprogsite.urls' - -# Кастомный middleware для CSP -class CSPMiddleware: - def __init__(self, get_response): - self.get_response = get_response - - def __call__(self, request): - response = self.get_response(request) - response['Content-Security-Policy'] = "frame-ancestors 'self' https://metrika.yandex.ru https://metrika.yandex.by https://metrica.yandex.com https://metrica.yandex.com.tr https://*.webvisor.com" - return response - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - 'programmer.context_processors.menu_processor', - 'programmer.context_processors.contact_info', - ], - }, - }, -] - -WSGI_APPLICATION = 'OneCprogsite.wsgi.application' - - -# Database -# https://docs.djangoproject.com/en/4.2/ref/settings/#databases - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': 'App', - 'USER': 'postgres', - 'PASSWORD': 'NikDi94Zell', - 'HOST': 'postgres', - 'PORT': 5432, - } -} - - -# Password validation -# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators - -AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', - }, -] - - -# Internationalization -# https://docs.djangoproject.com/en/4.2/topics/i18n/ - -LANGUAGE_CODE = 'ru' - -TIME_ZONE = 'Europe/Moscow' -USE_I18N = True -USE_I18N = True -USE_TZ = True - - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/4.2/howto/static-files/ - -STATIC_URL = 'static/' -STATIC_ROOT = os.path.join(BASE_DIR, 'static') -STATICFILES_DIRS = [] - -# Default primary key field type -# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field - -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' - -MEDIA_ROOT = os.path.join(BASE_DIR, 'media') -MEDIA_URL = '/media/' - -# Настройки email -EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' -# EMAIL_HOST = 'smtp.yandex.ru' # или smtp.gmail.com, smtp.mail.ru -# EMAIL_PORT = 587 -# EMAIL_USE_TLS = True -# EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER', 'it@yandex.ru') -# EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD', 'tifdctkrcjcqwxyc') -# DEFAULT_FROM_EMAIL = EMAIL_HOST_USER -# SERVER_EMAIL = EMAIL_HOST_USER - -EMAIL_HOST = 'smtp.gmail.com' # или smtp.gmail.com, smtp.mail.ru -EMAIL_PORT = 587 -EMAIL_USE_TLS = True -EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER', 'nikdizell@gmail.com') -EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD', 'qvmw yccb msqv mmpj') -DEFAULT_FROM_EMAIL = EMAIL_HOST_USER -SERVER_EMAIL = EMAIL_HOST_USER - -# Email для уведомлений (можно указать несколько через запятую) -# ADMIN_EMAILS = os.getenv('ADMIN_EMAILS', 'nikdizell@gmail.com').split(',') -ADMIN_EMAILS = os.getenv('ADMIN_EMAILS', 'it@nserdyuk.ru').split(',') - - +""" +Django settings for OneCprogsite project. + +Generated by 'django-admin startproject' using Django 4.2.7. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.2/ref/settings/ +""" +import os.path +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-5rs2a1*8cxjkv*%6k1-88biv&1#nep%@i+%1^dk=5j$s&e&hwm' + +# Безопасность cookies для HTTPS +SESSION_COOKIE_SECURE = True +CSRF_COOKIE_SECURE = True +SESSION_COOKIE_HTTPONLY = True +CSRF_COOKIE_HTTPONLY = False # Django требует доступ к CSRF cookie через JS +SESSION_COOKIE_SAMESITE = 'Lax' +CSRF_COOKIE_SAMESITE = 'Lax' + +# Если используете другие cookies +LANGUAGE_COOKIE_SECURE = True +LANGUAGE_COOKIE_HTTPONLY = True +LANGUAGE_COOKIE_SAMESITE = 'Lax' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = False + +X_FRAME_OPTIONS = 'SAMEORIGIN' +# Или разрешить конкретные домены (Django 4.0+) +X_FRAME_OPTIONS = 'ALLOW-FROM https://metrika.yandex.ru' + +# ОБЯЗАТЕЛЬНО укажите ваши домены +ALLOWED_HOSTS = [ + 'nikdizell.ru', + 'www.nikdizell.ru', + 'localhost', + '127.0.0.1', + '192.168.31.88' # Добавьте IP сервера +] + +# Важно для работы за прокси +SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') +SECURE_SSL_REDIRECT = True + +# Дополнительная безопасность +SECURE_BROWSER_XSS_FILTER = True +SECURE_CONTENT_TYPE_NOSNIFF = True +SECURE_HSTS_SECONDS = 31536000 +SECURE_HSTS_INCLUDE_SUBDOMAINS = True +SECURE_HSTS_PRELOAD = True + +CSRF_TRUSTED_ORIGINS = [ + 'https://nikdizell.ru', + 'https://www.nikdizell.ru', +] + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'programmer.apps.ProgrammerConfig', + 'django_bootstrap5', + 'django_extensions', + 'django.contrib.sites', + 'django.contrib.sitemaps', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'programmer.middleware.PageViewMiddleware', +] + +ROOT_URLCONF = 'OneCprogsite.urls' + +# Кастомный middleware для CSP +class CSPMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + response = self.get_response(request) + response['Content-Security-Policy'] = "frame-ancestors 'self' https://metrika.yandex.ru https://metrika.yandex.by https://metrica.yandex.com https://metrica.yandex.com.tr https://*.webvisor.com" + return response + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + 'programmer.context_processors.menu_processor', + 'programmer.context_processors.contact_info', + ], + }, + }, +] + +WSGI_APPLICATION = 'OneCprogsite.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/4.2/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': 'App', + 'USER': 'postgres', + 'PASSWORD': 'NikDi94Zell', + 'HOST': 'postgres', + 'PORT': 5432, + } +} + + +# Password validation +# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/4.2/topics/i18n/ + +LANGUAGE_CODE = 'ru' + +TIME_ZONE = 'Europe/Moscow' +USE_I18N = True +USE_I18N = True +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.2/howto/static-files/ + +STATIC_URL = 'static/' +STATIC_ROOT = os.path.join(BASE_DIR, 'static') +STATICFILES_DIRS = [] + +# Default primary key field type +# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +MEDIA_ROOT = os.path.join(BASE_DIR, 'media') +MEDIA_URL = '/media/' + +# Настройки email +EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' +# EMAIL_HOST = 'smtp.yandex.ru' # или smtp.gmail.com, smtp.mail.ru +# EMAIL_PORT = 587 +# EMAIL_USE_TLS = True +# EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER', 'it@yandex.ru') +# EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD', 'tifdctkrcjcqwxyc') +# DEFAULT_FROM_EMAIL = EMAIL_HOST_USER +# SERVER_EMAIL = EMAIL_HOST_USER + +EMAIL_HOST = 'smtp.gmail.com' # или smtp.gmail.com, smtp.mail.ru +EMAIL_PORT = 587 +EMAIL_USE_TLS = True +EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER', 'nikdizell@gmail.com') +EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD', 'qvmw yccb msqv mmpj') +DEFAULT_FROM_EMAIL = EMAIL_HOST_USER +SERVER_EMAIL = EMAIL_HOST_USER + +# Email для уведомлений (можно указать несколько через запятую) +# ADMIN_EMAILS = os.getenv('ADMIN_EMAILS', 'nikdizell@gmail.com').split(',') +ADMIN_EMAILS = os.getenv('ADMIN_EMAILS', 'it@nserdyuk.ru').split(',') + + diff --git a/OneCprogsite/static/django_extensions/css/jquery.autocomplete.css b/OneCprogsite/static/django_extensions/css/jquery.autocomplete.css index 0363616..8c76197 100644 --- a/OneCprogsite/static/django_extensions/css/jquery.autocomplete.css +++ b/OneCprogsite/static/django_extensions/css/jquery.autocomplete.css @@ -1,38 +1,38 @@ -/** - * @fileOverview CSS for jquery-autocomplete, the jQuery Autocompleter - * @author Dylan Verheul - * @license MIT | GPL | Apache 2.0, see LICENSE.txt - * @see https://github.com/dyve/jquery-autocomplete - */ -.acResults { - padding: 0px; - border: 1px solid WindowFrame; - background-color: Window; - overflow: hidden; -} - -.acResults ul { - margin: 0px; - padding: 0px; - list-style-position: outside; - list-style: none; -} - -.acResults ul li { - margin: 0px; - padding: 2px 5px; - cursor: pointer; - display: block; - font: menu; - font-size: 12px; - overflow: hidden; -} - -.acLoading { - background : url('../img/indicator.gif') right center no-repeat; -} - -.acSelect { - background-color: Highlight; - color: HighlightText; -} +/** + * @fileOverview CSS for jquery-autocomplete, the jQuery Autocompleter + * @author Dylan Verheul + * @license MIT | GPL | Apache 2.0, see LICENSE.txt + * @see https://github.com/dyve/jquery-autocomplete + */ +.acResults { + padding: 0px; + border: 1px solid WindowFrame; + background-color: Window; + overflow: hidden; +} + +.acResults ul { + margin: 0px; + padding: 0px; + list-style-position: outside; + list-style: none; +} + +.acResults ul li { + margin: 0px; + padding: 2px 5px; + cursor: pointer; + display: block; + font: menu; + font-size: 12px; + overflow: hidden; +} + +.acLoading { + background : url('../img/indicator.gif') right center no-repeat; +} + +.acSelect { + background-color: Highlight; + color: HighlightText; +} diff --git a/OneCprogsite/static/django_extensions/js/jquery.ajaxQueue.js b/OneCprogsite/static/django_extensions/js/jquery.ajaxQueue.js index aca15d9..2cfeda5 100644 --- a/OneCprogsite/static/django_extensions/js/jquery.ajaxQueue.js +++ b/OneCprogsite/static/django_extensions/js/jquery.ajaxQueue.js @@ -1,116 +1,116 @@ -/** - * Ajax Queue Plugin - */ - -/** - - -
      - - */ -/* - * Queued Ajax requests. - * A new Ajax request won't be started until the previous queued - * request has finished. - */ - -/* - * Synced Ajax requests. - * The Ajax request will happen as soon as you call this method, but - * the callbacks (success/error/complete) won't fire until all previous - * synced requests have been completed. - */ - - -(function(jQuery) { - - var ajax = jQuery.ajax; - - var pendingRequests = {}; - - var synced = []; - var syncedData = []; - - jQuery.ajax = function(settings) { - // create settings for compatibility with ajaxSetup - settings = jQuery.extend(settings, jQuery.extend({}, jQuery.ajaxSettings, settings)); - - var port = settings.port; - - switch(settings.mode) { - case "abort": - if ( pendingRequests[port] ) { - pendingRequests[port].abort(); - } - return pendingRequests[port] = ajax.apply(this, arguments); - case "queue": - var _old = settings.complete; - settings.complete = function(){ - if ( _old ) - _old.apply( this, arguments ); - jQuery([ajax]).dequeue("ajax" + port );; - }; - - jQuery([ ajax ]).queue("ajax" + port, function(){ - ajax( settings ); - }); - return; - case "sync": - var pos = synced.length; - - synced[ pos ] = { - error: settings.error, - success: settings.success, - complete: settings.complete, - done: false - }; - - syncedData[ pos ] = { - error: [], - success: [], - complete: [] - }; - - settings.error = function(){ syncedData[ pos ].error = arguments; }; - settings.success = function(){ syncedData[ pos ].success = arguments; }; - settings.complete = function(){ - syncedData[ pos ].complete = arguments; - synced[ pos ].done = true; - - if ( pos == 0 || !synced[ pos-1 ] ) - for ( var i = pos; i < synced.length && synced[i].done; i++ ) { - if ( synced[i].error ) synced[i].error.apply( jQuery, syncedData[i].error ); - if ( synced[i].success ) synced[i].success.apply( jQuery, syncedData[i].success ); - if ( synced[i].complete ) synced[i].complete.apply( jQuery, syncedData[i].complete ); - - synced[i] = null; - syncedData[i] = null; - } - }; - } - return ajax.apply(this, arguments); - }; - -})((typeof window.jQuery == 'undefined' && typeof window.django != 'undefined') - ? django.jQuery - : jQuery -); +/** + * Ajax Queue Plugin + */ + +/** + + +
        + + */ +/* + * Queued Ajax requests. + * A new Ajax request won't be started until the previous queued + * request has finished. + */ + +/* + * Synced Ajax requests. + * The Ajax request will happen as soon as you call this method, but + * the callbacks (success/error/complete) won't fire until all previous + * synced requests have been completed. + */ + + +(function(jQuery) { + + var ajax = jQuery.ajax; + + var pendingRequests = {}; + + var synced = []; + var syncedData = []; + + jQuery.ajax = function(settings) { + // create settings for compatibility with ajaxSetup + settings = jQuery.extend(settings, jQuery.extend({}, jQuery.ajaxSettings, settings)); + + var port = settings.port; + + switch(settings.mode) { + case "abort": + if ( pendingRequests[port] ) { + pendingRequests[port].abort(); + } + return pendingRequests[port] = ajax.apply(this, arguments); + case "queue": + var _old = settings.complete; + settings.complete = function(){ + if ( _old ) + _old.apply( this, arguments ); + jQuery([ajax]).dequeue("ajax" + port );; + }; + + jQuery([ ajax ]).queue("ajax" + port, function(){ + ajax( settings ); + }); + return; + case "sync": + var pos = synced.length; + + synced[ pos ] = { + error: settings.error, + success: settings.success, + complete: settings.complete, + done: false + }; + + syncedData[ pos ] = { + error: [], + success: [], + complete: [] + }; + + settings.error = function(){ syncedData[ pos ].error = arguments; }; + settings.success = function(){ syncedData[ pos ].success = arguments; }; + settings.complete = function(){ + syncedData[ pos ].complete = arguments; + synced[ pos ].done = true; + + if ( pos == 0 || !synced[ pos-1 ] ) + for ( var i = pos; i < synced.length && synced[i].done; i++ ) { + if ( synced[i].error ) synced[i].error.apply( jQuery, syncedData[i].error ); + if ( synced[i].success ) synced[i].success.apply( jQuery, syncedData[i].success ); + if ( synced[i].complete ) synced[i].complete.apply( jQuery, syncedData[i].complete ); + + synced[i] = null; + syncedData[i] = null; + } + }; + } + return ajax.apply(this, arguments); + }; + +})((typeof window.jQuery == 'undefined' && typeof window.django != 'undefined') + ? django.jQuery + : jQuery +); diff --git a/OneCprogsite/static/django_extensions/js/jquery.autocomplete.js b/OneCprogsite/static/django_extensions/js/jquery.autocomplete.js index 77c1db6..1cb879b 100644 --- a/OneCprogsite/static/django_extensions/js/jquery.autocomplete.js +++ b/OneCprogsite/static/django_extensions/js/jquery.autocomplete.js @@ -1,1152 +1,1152 @@ -/** - * @fileOverview jquery-autocomplete, the jQuery Autocompleter - * @author Dylan Verheul - * @version 2.4.4 - * @requires jQuery 1.6+ - * @license MIT | GPL | Apache 2.0, see LICENSE.txt - * @see https://github.com/dyve/jquery-autocomplete - */ -(function($) { - "use strict"; - - /** - * jQuery autocomplete plugin - * @param {object|string} options - * @returns (object} jQuery object - */ - $.fn.autocomplete = function(options) { - var url; - if (arguments.length > 1) { - url = options; - options = arguments[1]; - options.url = url; - } else if (typeof options === 'string') { - url = options; - options = { url: url }; - } - var opts = $.extend({}, $.fn.autocomplete.defaults, options); - return this.each(function() { - var $this = $(this); - $this.data('autocompleter', new $.Autocompleter( - $this, - $.meta ? $.extend({}, opts, $this.data()) : opts - )); - }); - }; - - /** - * Store default options - * @type {object} - */ - $.fn.autocomplete.defaults = { - inputClass: 'acInput', - loadingClass: 'acLoading', - resultsClass: 'acResults', - selectClass: 'acSelect', - queryParamName: 'q', - extraParams: {}, - remoteDataType: false, - lineSeparator: '\n', - cellSeparator: '|', - minChars: 2, - maxItemsToShow: 10, - delay: 400, - useCache: true, - maxCacheLength: 10, - matchSubset: true, - matchCase: false, - matchInside: true, - mustMatch: false, - selectFirst: false, - selectOnly: false, - showResult: null, - preventDefaultReturn: 1, - preventDefaultTab: 0, - autoFill: false, - filterResults: true, - filter: true, - sortResults: true, - sortFunction: null, - onItemSelect: null, - onNoMatch: null, - onFinish: null, - matchStringConverter: null, - beforeUseConverter: null, - autoWidth: 'min-width', - useDelimiter: false, - delimiterChar: ',', - delimiterKeyCode: 188, - processData: null, - onError: null, - enabled: true - }; - - /** - * Sanitize result - * @param {Object} result - * @returns {Object} object with members value (String) and data (Object) - * @private - */ - var sanitizeResult = function(result) { - var value, data; - var type = typeof result; - if (type === 'string') { - value = result; - data = {}; - } else if ($.isArray(result)) { - value = result[0]; - data = result.slice(1); - } else if (type === 'object') { - value = result.value; - data = result.data; - } - value = String(value); - if (typeof data !== 'object') { - data = {}; - } - return { - value: value, - data: data - }; - }; - - /** - * Sanitize integer - * @param {mixed} value - * @param {Object} options - * @returns {Number} integer - * @private - */ - var sanitizeInteger = function(value, stdValue, options) { - var num = parseInt(value, 10); - options = options || {}; - if (isNaN(num) || (options.min && num < options.min)) { - num = stdValue; - } - return num; - }; - - /** - * Create partial url for a name/value pair - */ - var makeUrlParam = function(name, value) { - return [name, encodeURIComponent(value)].join('='); - }; - - /** - * Build an url - * @param {string} url Base url - * @param {object} [params] Dictionary of parameters - */ - var makeUrl = function(url, params) { - var urlAppend = []; - $.each(params, function(index, value) { - urlAppend.push(makeUrlParam(index, value)); - }); - if (urlAppend.length) { - url += url.indexOf('?') === -1 ? '?' : '&'; - url += urlAppend.join('&'); - } - return url; - }; - - /** - * Default sort filter - * @param {object} a - * @param {object} b - * @param {boolean} matchCase - * @returns {number} - */ - var sortValueAlpha = function(a, b, matchCase) { - a = String(a.value); - b = String(b.value); - if (!matchCase) { - a = a.toLowerCase(); - b = b.toLowerCase(); - } - if (a > b) { - return 1; - } - if (a < b) { - return -1; - } - return 0; - }; - - /** - * Parse data received in text format - * @param {string} text Plain text input - * @param {string} lineSeparator String that separates lines - * @param {string} cellSeparator String that separates cells - * @returns {array} Array of autocomplete data objects - */ - var plainTextParser = function(text, lineSeparator, cellSeparator) { - var results = []; - var i, j, data, line, value, lines; - // Be nice, fix linebreaks before splitting on lineSeparator - lines = String(text).replace('\r\n', '\n').split(lineSeparator); - for (i = 0; i < lines.length; i++) { - line = lines[i].split(cellSeparator); - data = []; - for (j = 0; j < line.length; j++) { - data.push(decodeURIComponent(line[j])); - } - value = data.shift(); - results.push({ value: value, data: data }); - } - return results; - }; - - /** - * Autocompleter class - * @param {object} $elem jQuery object with one input tag - * @param {object} options Settings - * @constructor - */ - $.Autocompleter = function($elem, options) { - - /** - * Assert parameters - */ - if (!$elem || !($elem instanceof $) || $elem.length !== 1 || $elem.get(0).tagName.toUpperCase() !== 'INPUT') { - throw new Error('Invalid parameter for jquery.Autocompleter, jQuery object with one element with INPUT tag expected.'); - } - - /** - * @constant Link to this instance - * @type object - * @private - */ - var self = this; - - /** - * @property {object} Options for this instance - * @public - */ - this.options = options; - - /** - * @property object Cached data for this instance - * @private - */ - this.cacheData_ = {}; - - /** - * @property {number} Number of cached data items - * @private - */ - this.cacheLength_ = 0; - - /** - * @property {string} Class name to mark selected item - * @private - */ - this.selectClass_ = 'jquery-autocomplete-selected-item'; - - /** - * @property {number} Handler to activation timeout - * @private - */ - this.keyTimeout_ = null; - - /** - * @property {number} Handler to finish timeout - * @private - */ - this.finishTimeout_ = null; - - /** - * @property {number} Last key pressed in the input field (store for behavior) - * @private - */ - this.lastKeyPressed_ = null; - - /** - * @property {string} Last value processed by the autocompleter - * @private - */ - this.lastProcessedValue_ = null; - - /** - * @property {string} Last value selected by the user - * @private - */ - this.lastSelectedValue_ = null; - - /** - * @property {boolean} Is this autocompleter active (showing results)? - * @see showResults - * @private - */ - this.active_ = false; - - /** - * @property {boolean} Is this autocompleter allowed to finish on blur? - * @private - */ - this.finishOnBlur_ = true; - - /** - * Sanitize options - */ - this.options.minChars = sanitizeInteger(this.options.minChars, $.fn.autocomplete.defaults.minChars, { min: 0 }); - this.options.maxItemsToShow = sanitizeInteger(this.options.maxItemsToShow, $.fn.autocomplete.defaults.maxItemsToShow, { min: 0 }); - this.options.maxCacheLength = sanitizeInteger(this.options.maxCacheLength, $.fn.autocomplete.defaults.maxCacheLength, { min: 1 }); - this.options.delay = sanitizeInteger(this.options.delay, $.fn.autocomplete.defaults.delay, { min: 0 }); - if (this.options.preventDefaultReturn != 2) { - this.options.preventDefaultReturn = this.options.preventDefaultReturn ? 1 : 0; - } - if (this.options.preventDefaultTab != 2) { - this.options.preventDefaultTab = this.options.preventDefaultTab ? 1 : 0; - } - - /** - * Init DOM elements repository - */ - this.dom = {}; - - /** - * Store the input element we're attached to in the repository - */ - this.dom.$elem = $elem; - - /** - * Switch off the native autocomplete and add the input class - */ - this.dom.$elem.attr('autocomplete', 'off').addClass(this.options.inputClass); - - /** - * Create DOM element to hold results, and force absolute position - */ - this.dom.$results = $('
        ').hide().addClass(this.options.resultsClass).css({ - position: 'absolute' - }); - $('body').append(this.dom.$results); - - /** - * Attach keyboard monitoring to $elem - */ - $elem.keydown(function(e) { - self.lastKeyPressed_ = e.keyCode; - switch(self.lastKeyPressed_) { - - case self.options.delimiterKeyCode: // comma = 188 - if (self.options.useDelimiter && self.active_) { - self.selectCurrent(); - } - break; - - // ignore navigational & special keys - case 35: // end - case 36: // home - case 16: // shift - case 17: // ctrl - case 18: // alt - case 37: // left - case 39: // right - break; - - case 38: // up - e.preventDefault(); - if (self.active_) { - self.focusPrev(); - } else { - self.activate(); - } - return false; - - case 40: // down - e.preventDefault(); - if (self.active_) { - self.focusNext(); - } else { - self.activate(); - } - return false; - - case 9: // tab - if (self.active_) { - self.selectCurrent(); - if (self.options.preventDefaultTab) { - e.preventDefault(); - return false; - } - } - if (self.options.preventDefaultTab === 2) { - e.preventDefault(); - return false; - } - break; - - case 13: // return - if (self.active_) { - self.selectCurrent(); - if (self.options.preventDefaultReturn) { - e.preventDefault(); - return false; - } - } - if (self.options.preventDefaultReturn === 2) { - e.preventDefault(); - return false; - } - break; - - case 27: // escape - if (self.active_) { - e.preventDefault(); - self.deactivate(true); - return false; - } - break; - - default: - self.activate(); - - } - }); - - /** - * Attach paste event listener because paste may occur much later then keydown or even without a keydown at all - */ - $elem.on('paste', function() { - self.activate(); - }); - - /** - * Finish on blur event - * Use a timeout because instant blur gives race conditions - */ - var onBlurFunction = function() { - self.deactivate(true); - } - $elem.blur(function() { - if (self.finishOnBlur_) { - self.finishTimeout_ = setTimeout(onBlurFunction, 200); - } - }); - /** - * Catch a race condition on form submit - */ - $elem.parents('form').on('submit', onBlurFunction); - - }; - - /** - * Position output DOM elements - * @private - */ - $.Autocompleter.prototype.position = function() { - var offset = this.dom.$elem.offset(); - var height = this.dom.$results.outerHeight(); - var totalHeight = $(window).outerHeight(); - var inputBottom = offset.top + this.dom.$elem.outerHeight(); - var bottomIfDown = inputBottom + height; - // Set autocomplete results at the bottom of input - var position = {top: inputBottom, left: offset.left}; - if (bottomIfDown > totalHeight) { - // Try to set autocomplete results at the top of input - var topIfUp = offset.top - height; - if (topIfUp >= 0) { - position.top = topIfUp; - } - } - this.dom.$results.css(position); - }; - - /** - * Read from cache - * @private - */ - $.Autocompleter.prototype.cacheRead = function(filter) { - var filterLength, searchLength, search, maxPos, pos; - if (this.options.useCache) { - filter = String(filter); - filterLength = filter.length; - if (this.options.matchSubset) { - searchLength = 1; - } else { - searchLength = filterLength; - } - while (searchLength <= filterLength) { - if (this.options.matchInside) { - maxPos = filterLength - searchLength; - } else { - maxPos = 0; - } - pos = 0; - while (pos <= maxPos) { - search = filter.substr(0, searchLength); - if (this.cacheData_[search] !== undefined) { - return this.cacheData_[search]; - } - pos++; - } - searchLength++; - } - } - return false; - }; - - /** - * Write to cache - * @private - */ - $.Autocompleter.prototype.cacheWrite = function(filter, data) { - if (this.options.useCache) { - if (this.cacheLength_ >= this.options.maxCacheLength) { - this.cacheFlush(); - } - filter = String(filter); - if (this.cacheData_[filter] !== undefined) { - this.cacheLength_++; - } - this.cacheData_[filter] = data; - return this.cacheData_[filter]; - } - return false; - }; - - /** - * Flush cache - * @public - */ - $.Autocompleter.prototype.cacheFlush = function() { - this.cacheData_ = {}; - this.cacheLength_ = 0; - }; - - /** - * Call hook - * Note that all called hooks are passed the autocompleter object - * @param {string} hook - * @param data - * @returns Result of called hook, false if hook is undefined - */ - $.Autocompleter.prototype.callHook = function(hook, data) { - var f = this.options[hook]; - if (f && $.isFunction(f)) { - return f(data, this); - } - return false; - }; - - /** - * Set timeout to activate autocompleter - */ - $.Autocompleter.prototype.activate = function() { - if (!this.options.enabled) return; - var self = this; - if (this.keyTimeout_) { - clearTimeout(this.keyTimeout_); - } - this.keyTimeout_ = setTimeout(function() { - self.activateNow(); - }, this.options.delay); - }; - - /** - * Activate autocompleter immediately - */ - $.Autocompleter.prototype.activateNow = function() { - var value = this.beforeUseConverter(this.dom.$elem.val()); - if (value !== this.lastProcessedValue_ && value !== this.lastSelectedValue_) { - this.fetchData(value); - } - }; - - /** - * Get autocomplete data for a given value - * @param {string} value Value to base autocompletion on - * @private - */ - $.Autocompleter.prototype.fetchData = function(value) { - var self = this; - var processResults = function(results, filter) { - if (self.options.processData) { - results = self.options.processData(results); - } - self.showResults(self.filterResults(results, filter), filter); - }; - this.lastProcessedValue_ = value; - if (value.length < this.options.minChars) { - processResults([], value); - } else if (this.options.data) { - processResults(this.options.data, value); - } else { - this.fetchRemoteData(value, function(remoteData) { - processResults(remoteData, value); - }); - } - }; - - /** - * Get remote autocomplete data for a given value - * @param {string} filter The filter to base remote data on - * @param {function} callback The function to call after data retrieval - * @private - */ - $.Autocompleter.prototype.fetchRemoteData = function(filter, callback) { - var data = this.cacheRead(filter); - if (data) { - callback(data); - } else { - var self = this; - var dataType = self.options.remoteDataType === 'json' ? 'json' : 'text'; - var ajaxCallback = function(data) { - var parsed = false; - if (data !== false) { - parsed = self.parseRemoteData(data); - self.cacheWrite(filter, parsed); - } - self.dom.$elem.removeClass(self.options.loadingClass); - callback(parsed); - }; - this.dom.$elem.addClass(this.options.loadingClass); - $.ajax({ - url: this.makeUrl(filter), - success: ajaxCallback, - error: function(jqXHR, textStatus, errorThrown) { - if($.isFunction(self.options.onError)) { - self.options.onError(jqXHR, textStatus, errorThrown); - } else { - ajaxCallback(false); - } - }, - dataType: dataType - }); - } - }; - - /** - * Create or update an extra parameter for the remote request - * @param {string} name Parameter name - * @param {string} value Parameter value - * @public - */ - $.Autocompleter.prototype.setExtraParam = function(name, value) { - var index = $.trim(String(name)); - if (index) { - if (!this.options.extraParams) { - this.options.extraParams = {}; - } - if (this.options.extraParams[index] !== value) { - this.options.extraParams[index] = value; - this.cacheFlush(); - } - } - - return this; - }; - - /** - * Build the url for a remote request - * If options.queryParamName === false, append query to url instead of using a GET parameter - * @param {string} param The value parameter to pass to the backend - * @returns {string} The finished url with parameters - */ - $.Autocompleter.prototype.makeUrl = function(param) { - var self = this; - var url = this.options.url; - var params = $.extend({}, this.options.extraParams); - - if (this.options.queryParamName === false) { - url += encodeURIComponent(param); - } else { - params[this.options.queryParamName] = param; - } - - return makeUrl(url, params); - }; - - /** - * Parse data received from server - * @param remoteData Data received from remote server - * @returns {array} Parsed data - */ - $.Autocompleter.prototype.parseRemoteData = function(remoteData) { - var remoteDataType; - var data = remoteData; - if (this.options.remoteDataType === 'json') { - remoteDataType = typeof(remoteData); - switch (remoteDataType) { - case 'object': - data = remoteData; - break; - case 'string': - data = $.parseJSON(remoteData); - break; - default: - throw new Error("Unexpected remote data type: " + remoteDataType); - } - return data; - } - return plainTextParser(data, this.options.lineSeparator, this.options.cellSeparator); - }; - - /** - * Default filter for results - * @param {Object} result - * @param {String} filter - * @returns {boolean} Include this result - * @private - */ - $.Autocompleter.prototype.defaultFilter = function(result, filter) { - if (!result.value) { - return false; - } - if (this.options.filterResults) { - var pattern = this.matchStringConverter(filter); - var testValue = this.matchStringConverter(result.value); - if (!this.options.matchCase) { - pattern = pattern.toLowerCase(); - testValue = testValue.toLowerCase(); - } - var patternIndex = testValue.indexOf(pattern); - if (this.options.matchInside) { - return patternIndex > -1; - } else { - return patternIndex === 0; - } - } - return true; - }; - - /** - * Filter result - * @param {Object} result - * @param {String} filter - * @returns {boolean} Include this result - * @private - */ - $.Autocompleter.prototype.filterResult = function(result, filter) { - // No filter - if (this.options.filter === false) { - return true; - } - // Custom filter - if ($.isFunction(this.options.filter)) { - return this.options.filter(result, filter); - } - // Default filter - return this.defaultFilter(result, filter); - }; - - /** - * Filter results - * @param results - * @param filter - */ - $.Autocompleter.prototype.filterResults = function(results, filter) { - var filtered = []; - var i, result; - - for (i = 0; i < results.length; i++) { - result = sanitizeResult(results[i]); - if (this.filterResult(result, filter)) { - filtered.push(result); - } - } - if (this.options.sortResults) { - filtered = this.sortResults(filtered, filter); - } - if (this.options.maxItemsToShow > 0 && this.options.maxItemsToShow < filtered.length) { - filtered.length = this.options.maxItemsToShow; - } - return filtered; - }; - - /** - * Sort results - * @param results - * @param filter - */ - $.Autocompleter.prototype.sortResults = function(results, filter) { - var self = this; - var sortFunction = this.options.sortFunction; - if (!$.isFunction(sortFunction)) { - sortFunction = function(a, b, f) { - return sortValueAlpha(a, b, self.options.matchCase); - }; - } - results.sort(function(a, b) { - return sortFunction(a, b, filter, self.options); - }); - return results; - }; - - /** - * Convert string before matching - * @param s - * @param a - * @param b - */ - $.Autocompleter.prototype.matchStringConverter = function(s, a, b) { - var converter = this.options.matchStringConverter; - if ($.isFunction(converter)) { - s = converter(s, a, b); - } - return s; - }; - - /** - * Convert string before use - * @param {String} s - */ - $.Autocompleter.prototype.beforeUseConverter = function(s) { - s = this.getValue(s); - var converter = this.options.beforeUseConverter; - if ($.isFunction(converter)) { - s = converter(s); - } - return s; - }; - - /** - * Enable finish on blur event - */ - $.Autocompleter.prototype.enableFinishOnBlur = function() { - this.finishOnBlur_ = true; - }; - - /** - * Disable finish on blur event - */ - $.Autocompleter.prototype.disableFinishOnBlur = function() { - this.finishOnBlur_ = false; - }; - - /** - * Create a results item (LI element) from a result - * @param result - */ - $.Autocompleter.prototype.createItemFromResult = function(result) { - var self = this; - var $li = $('
      • '); - $li.html(this.showResult(result.value, result.data)); - $li.data({value: result.value, data: result.data}) - .click(function() { - self.selectItem($li); - }) - .mousedown(self.disableFinishOnBlur) - .mouseup(self.enableFinishOnBlur) - ; - return $li; - }; - - /** - * Get all items from the results list - * @param result - */ - $.Autocompleter.prototype.getItems = function() { - return $('>ul>li', this.dom.$results); - }; - - /** - * Show all results - * @param results - * @param filter - */ - $.Autocompleter.prototype.showResults = function(results, filter) { - var numResults = results.length; - var self = this; - var $ul = $('
          '); - var i, result, $li, autoWidth, first = false, $first = false; - - if (numResults) { - for (i = 0; i < numResults; i++) { - result = results[i]; - $li = this.createItemFromResult(result); - $ul.append($li); - if (first === false) { - first = String(result.value); - $first = $li; - $li.addClass(this.options.firstItemClass); - } - if (i === numResults - 1) { - $li.addClass(this.options.lastItemClass); - } - } - - this.dom.$results.html($ul).show(); - - // Always recalculate position since window size or - // input element location may have changed. - this.position(); - if (this.options.autoWidth) { - autoWidth = this.dom.$elem.outerWidth() - this.dom.$results.outerWidth() + this.dom.$results.width(); - this.dom.$results.css(this.options.autoWidth, autoWidth); - } - this.getItems().hover( - function() { self.focusItem(this); }, - function() { /* void */ } - ); - if (this.autoFill(first, filter) || this.options.selectFirst || (this.options.selectOnly && numResults === 1)) { - this.focusItem($first); - } - this.active_ = true; - } else { - this.hideResults(); - this.active_ = false; - } - }; - - $.Autocompleter.prototype.showResult = function(value, data) { - if ($.isFunction(this.options.showResult)) { - return this.options.showResult(value, data); - } else { - return $('

          ').text(value).html(); - } - }; - - $.Autocompleter.prototype.autoFill = function(value, filter) { - var lcValue, lcFilter, valueLength, filterLength; - if (this.options.autoFill && this.lastKeyPressed_ !== 8) { - lcValue = String(value).toLowerCase(); - lcFilter = String(filter).toLowerCase(); - valueLength = value.length; - filterLength = filter.length; - if (lcValue.substr(0, filterLength) === lcFilter) { - var d = this.getDelimiterOffsets(); - var pad = d.start ? ' ' : ''; // if there is a preceding delimiter - this.setValue( pad + value ); - var start = filterLength + d.start + pad.length; - var end = valueLength + d.start + pad.length; - this.selectRange(start, end); - return true; - } - } - return false; - }; - - $.Autocompleter.prototype.focusNext = function() { - this.focusMove(+1); - }; - - $.Autocompleter.prototype.focusPrev = function() { - this.focusMove(-1); - }; - - $.Autocompleter.prototype.focusMove = function(modifier) { - var $items = this.getItems(); - modifier = sanitizeInteger(modifier, 0); - if (modifier) { - for (var i = 0; i < $items.length; i++) { - if ($($items[i]).hasClass(this.selectClass_)) { - this.focusItem(i + modifier); - return; - } - } - } - this.focusItem(0); - }; - - $.Autocompleter.prototype.focusItem = function(item) { - var $item, $items = this.getItems(); - if ($items.length) { - $items.removeClass(this.selectClass_).removeClass(this.options.selectClass); - if (typeof item === 'number') { - if (item < 0) { - item = 0; - } else if (item >= $items.length) { - item = $items.length - 1; - } - $item = $($items[item]); - } else { - $item = $(item); - } - if ($item) { - $item.addClass(this.selectClass_).addClass(this.options.selectClass); - } - } - }; - - $.Autocompleter.prototype.selectCurrent = function() { - var $item = $('li.' + this.selectClass_, this.dom.$results); - if ($item.length === 1) { - this.selectItem($item); - } else { - this.deactivate(false); - } - }; - - $.Autocompleter.prototype.selectItem = function($li) { - var value = $li.data('value'); - var data = $li.data('data'); - var displayValue = this.displayValue(value, data); - var processedDisplayValue = this.beforeUseConverter(displayValue); - this.lastProcessedValue_ = processedDisplayValue; - this.lastSelectedValue_ = processedDisplayValue; - var d = this.getDelimiterOffsets(); - var delimiter = this.options.delimiterChar; - var elem = this.dom.$elem; - var extraCaretPos = 0; - if ( this.options.useDelimiter ) { - // if there is a preceding delimiter, add a space after the delimiter - if ( elem.val().substring(d.start-1, d.start) == delimiter && delimiter != ' ' ) { - displayValue = ' ' + displayValue; - } - // if there is not already a delimiter trailing this value, add it - if ( elem.val().substring(d.end, d.end+1) != delimiter && this.lastKeyPressed_ != this.options.delimiterKeyCode ) { - displayValue = displayValue + delimiter; - } else { - // move the cursor after the existing trailing delimiter - extraCaretPos = 1; - } - } - this.setValue(displayValue); - this.setCaret(d.start + displayValue.length + extraCaretPos); - this.callHook('onItemSelect', { value: value, data: data }); - this.deactivate(true); - elem.focus(); - }; - - $.Autocompleter.prototype.displayValue = function(value, data) { - if ($.isFunction(this.options.displayValue)) { - return this.options.displayValue(value, data); - } - return value; - }; - - $.Autocompleter.prototype.hideResults = function() { - this.dom.$results.hide(); - }; - - $.Autocompleter.prototype.deactivate = function(finish) { - if (this.finishTimeout_) { - clearTimeout(this.finishTimeout_); - } - if (this.keyTimeout_) { - clearTimeout(this.keyTimeout_); - } - if (finish) { - if (this.lastProcessedValue_ !== this.lastSelectedValue_) { - if (this.options.mustMatch) { - this.setValue(''); - } - this.callHook('onNoMatch'); - } - if (this.active_) { - this.callHook('onFinish'); - } - this.lastKeyPressed_ = null; - this.lastProcessedValue_ = null; - this.lastSelectedValue_ = null; - this.active_ = false; - } - this.hideResults(); - }; - - $.Autocompleter.prototype.selectRange = function(start, end) { - var input = this.dom.$elem.get(0); - if (input.setSelectionRange) { - input.focus(); - input.setSelectionRange(start, end); - } else if (input.createTextRange) { - var range = input.createTextRange(); - range.collapse(true); - range.moveEnd('character', end); - range.moveStart('character', start); - range.select(); - } - }; - - /** - * Move caret to position - * @param {Number} pos - */ - $.Autocompleter.prototype.setCaret = function(pos) { - this.selectRange(pos, pos); - }; - - /** - * Get caret position - */ - $.Autocompleter.prototype.getCaret = function() { - var $elem = this.dom.$elem; - var elem = $elem[0]; - var val, selection, range, start, end, stored_range; - if (elem.createTextRange) { // IE - selection = document.selection; - if (elem.tagName.toLowerCase() != 'textarea') { - val = $elem.val(); - range = selection.createRange().duplicate(); - range.moveEnd('character', val.length); - if (range.text === '') { - start = val.length; - } else { - start = val.lastIndexOf(range.text); - } - range = selection.createRange().duplicate(); - range.moveStart('character', -val.length); - end = range.text.length; - } else { - range = selection.createRange(); - stored_range = range.duplicate(); - stored_range.moveToElementText(elem); - stored_range.setEndPoint('EndToEnd', range); - start = stored_range.text.length - range.text.length; - end = start + range.text.length; - } - } else { - start = $elem[0].selectionStart; - end = $elem[0].selectionEnd; - } - return { - start: start, - end: end - }; - }; - - /** - * Set the value that is currently being autocompleted - * @param {String} value - */ - $.Autocompleter.prototype.setValue = function(value) { - if ( this.options.useDelimiter ) { - // set the substring between the current delimiters - var val = this.dom.$elem.val(); - var d = this.getDelimiterOffsets(); - var preVal = val.substring(0, d.start); - var postVal = val.substring(d.end); - value = preVal + value + postVal; - } - this.dom.$elem.val(value); - }; - - /** - * Get the value currently being autocompleted - * @param {String} value - */ - $.Autocompleter.prototype.getValue = function(value) { - if ( this.options.useDelimiter ) { - var d = this.getDelimiterOffsets(); - return value.substring(d.start, d.end).trim(); - } else { - return value; - } - }; - - /** - * Get the offsets of the value currently being autocompleted - */ - $.Autocompleter.prototype.getDelimiterOffsets = function() { - var val = this.dom.$elem.val(); - if ( this.options.useDelimiter ) { - var preCaretVal = val.substring(0, this.getCaret().start); - var start = preCaretVal.lastIndexOf(this.options.delimiterChar) + 1; - var postCaretVal = val.substring(this.getCaret().start); - var end = postCaretVal.indexOf(this.options.delimiterChar); - if ( end == -1 ) end = val.length; - end += this.getCaret().start; - } else { - start = 0; - end = val.length; - } - return { - start: start, - end: end - }; - }; - -})((typeof window.jQuery == 'undefined' && typeof window.django != 'undefined')? django.jQuery : jQuery); +/** + * @fileOverview jquery-autocomplete, the jQuery Autocompleter + * @author Dylan Verheul + * @version 2.4.4 + * @requires jQuery 1.6+ + * @license MIT | GPL | Apache 2.0, see LICENSE.txt + * @see https://github.com/dyve/jquery-autocomplete + */ +(function($) { + "use strict"; + + /** + * jQuery autocomplete plugin + * @param {object|string} options + * @returns (object} jQuery object + */ + $.fn.autocomplete = function(options) { + var url; + if (arguments.length > 1) { + url = options; + options = arguments[1]; + options.url = url; + } else if (typeof options === 'string') { + url = options; + options = { url: url }; + } + var opts = $.extend({}, $.fn.autocomplete.defaults, options); + return this.each(function() { + var $this = $(this); + $this.data('autocompleter', new $.Autocompleter( + $this, + $.meta ? $.extend({}, opts, $this.data()) : opts + )); + }); + }; + + /** + * Store default options + * @type {object} + */ + $.fn.autocomplete.defaults = { + inputClass: 'acInput', + loadingClass: 'acLoading', + resultsClass: 'acResults', + selectClass: 'acSelect', + queryParamName: 'q', + extraParams: {}, + remoteDataType: false, + lineSeparator: '\n', + cellSeparator: '|', + minChars: 2, + maxItemsToShow: 10, + delay: 400, + useCache: true, + maxCacheLength: 10, + matchSubset: true, + matchCase: false, + matchInside: true, + mustMatch: false, + selectFirst: false, + selectOnly: false, + showResult: null, + preventDefaultReturn: 1, + preventDefaultTab: 0, + autoFill: false, + filterResults: true, + filter: true, + sortResults: true, + sortFunction: null, + onItemSelect: null, + onNoMatch: null, + onFinish: null, + matchStringConverter: null, + beforeUseConverter: null, + autoWidth: 'min-width', + useDelimiter: false, + delimiterChar: ',', + delimiterKeyCode: 188, + processData: null, + onError: null, + enabled: true + }; + + /** + * Sanitize result + * @param {Object} result + * @returns {Object} object with members value (String) and data (Object) + * @private + */ + var sanitizeResult = function(result) { + var value, data; + var type = typeof result; + if (type === 'string') { + value = result; + data = {}; + } else if ($.isArray(result)) { + value = result[0]; + data = result.slice(1); + } else if (type === 'object') { + value = result.value; + data = result.data; + } + value = String(value); + if (typeof data !== 'object') { + data = {}; + } + return { + value: value, + data: data + }; + }; + + /** + * Sanitize integer + * @param {mixed} value + * @param {Object} options + * @returns {Number} integer + * @private + */ + var sanitizeInteger = function(value, stdValue, options) { + var num = parseInt(value, 10); + options = options || {}; + if (isNaN(num) || (options.min && num < options.min)) { + num = stdValue; + } + return num; + }; + + /** + * Create partial url for a name/value pair + */ + var makeUrlParam = function(name, value) { + return [name, encodeURIComponent(value)].join('='); + }; + + /** + * Build an url + * @param {string} url Base url + * @param {object} [params] Dictionary of parameters + */ + var makeUrl = function(url, params) { + var urlAppend = []; + $.each(params, function(index, value) { + urlAppend.push(makeUrlParam(index, value)); + }); + if (urlAppend.length) { + url += url.indexOf('?') === -1 ? '?' : '&'; + url += urlAppend.join('&'); + } + return url; + }; + + /** + * Default sort filter + * @param {object} a + * @param {object} b + * @param {boolean} matchCase + * @returns {number} + */ + var sortValueAlpha = function(a, b, matchCase) { + a = String(a.value); + b = String(b.value); + if (!matchCase) { + a = a.toLowerCase(); + b = b.toLowerCase(); + } + if (a > b) { + return 1; + } + if (a < b) { + return -1; + } + return 0; + }; + + /** + * Parse data received in text format + * @param {string} text Plain text input + * @param {string} lineSeparator String that separates lines + * @param {string} cellSeparator String that separates cells + * @returns {array} Array of autocomplete data objects + */ + var plainTextParser = function(text, lineSeparator, cellSeparator) { + var results = []; + var i, j, data, line, value, lines; + // Be nice, fix linebreaks before splitting on lineSeparator + lines = String(text).replace('\r\n', '\n').split(lineSeparator); + for (i = 0; i < lines.length; i++) { + line = lines[i].split(cellSeparator); + data = []; + for (j = 0; j < line.length; j++) { + data.push(decodeURIComponent(line[j])); + } + value = data.shift(); + results.push({ value: value, data: data }); + } + return results; + }; + + /** + * Autocompleter class + * @param {object} $elem jQuery object with one input tag + * @param {object} options Settings + * @constructor + */ + $.Autocompleter = function($elem, options) { + + /** + * Assert parameters + */ + if (!$elem || !($elem instanceof $) || $elem.length !== 1 || $elem.get(0).tagName.toUpperCase() !== 'INPUT') { + throw new Error('Invalid parameter for jquery.Autocompleter, jQuery object with one element with INPUT tag expected.'); + } + + /** + * @constant Link to this instance + * @type object + * @private + */ + var self = this; + + /** + * @property {object} Options for this instance + * @public + */ + this.options = options; + + /** + * @property object Cached data for this instance + * @private + */ + this.cacheData_ = {}; + + /** + * @property {number} Number of cached data items + * @private + */ + this.cacheLength_ = 0; + + /** + * @property {string} Class name to mark selected item + * @private + */ + this.selectClass_ = 'jquery-autocomplete-selected-item'; + + /** + * @property {number} Handler to activation timeout + * @private + */ + this.keyTimeout_ = null; + + /** + * @property {number} Handler to finish timeout + * @private + */ + this.finishTimeout_ = null; + + /** + * @property {number} Last key pressed in the input field (store for behavior) + * @private + */ + this.lastKeyPressed_ = null; + + /** + * @property {string} Last value processed by the autocompleter + * @private + */ + this.lastProcessedValue_ = null; + + /** + * @property {string} Last value selected by the user + * @private + */ + this.lastSelectedValue_ = null; + + /** + * @property {boolean} Is this autocompleter active (showing results)? + * @see showResults + * @private + */ + this.active_ = false; + + /** + * @property {boolean} Is this autocompleter allowed to finish on blur? + * @private + */ + this.finishOnBlur_ = true; + + /** + * Sanitize options + */ + this.options.minChars = sanitizeInteger(this.options.minChars, $.fn.autocomplete.defaults.minChars, { min: 0 }); + this.options.maxItemsToShow = sanitizeInteger(this.options.maxItemsToShow, $.fn.autocomplete.defaults.maxItemsToShow, { min: 0 }); + this.options.maxCacheLength = sanitizeInteger(this.options.maxCacheLength, $.fn.autocomplete.defaults.maxCacheLength, { min: 1 }); + this.options.delay = sanitizeInteger(this.options.delay, $.fn.autocomplete.defaults.delay, { min: 0 }); + if (this.options.preventDefaultReturn != 2) { + this.options.preventDefaultReturn = this.options.preventDefaultReturn ? 1 : 0; + } + if (this.options.preventDefaultTab != 2) { + this.options.preventDefaultTab = this.options.preventDefaultTab ? 1 : 0; + } + + /** + * Init DOM elements repository + */ + this.dom = {}; + + /** + * Store the input element we're attached to in the repository + */ + this.dom.$elem = $elem; + + /** + * Switch off the native autocomplete and add the input class + */ + this.dom.$elem.attr('autocomplete', 'off').addClass(this.options.inputClass); + + /** + * Create DOM element to hold results, and force absolute position + */ + this.dom.$results = $('
          ').hide().addClass(this.options.resultsClass).css({ + position: 'absolute' + }); + $('body').append(this.dom.$results); + + /** + * Attach keyboard monitoring to $elem + */ + $elem.keydown(function(e) { + self.lastKeyPressed_ = e.keyCode; + switch(self.lastKeyPressed_) { + + case self.options.delimiterKeyCode: // comma = 188 + if (self.options.useDelimiter && self.active_) { + self.selectCurrent(); + } + break; + + // ignore navigational & special keys + case 35: // end + case 36: // home + case 16: // shift + case 17: // ctrl + case 18: // alt + case 37: // left + case 39: // right + break; + + case 38: // up + e.preventDefault(); + if (self.active_) { + self.focusPrev(); + } else { + self.activate(); + } + return false; + + case 40: // down + e.preventDefault(); + if (self.active_) { + self.focusNext(); + } else { + self.activate(); + } + return false; + + case 9: // tab + if (self.active_) { + self.selectCurrent(); + if (self.options.preventDefaultTab) { + e.preventDefault(); + return false; + } + } + if (self.options.preventDefaultTab === 2) { + e.preventDefault(); + return false; + } + break; + + case 13: // return + if (self.active_) { + self.selectCurrent(); + if (self.options.preventDefaultReturn) { + e.preventDefault(); + return false; + } + } + if (self.options.preventDefaultReturn === 2) { + e.preventDefault(); + return false; + } + break; + + case 27: // escape + if (self.active_) { + e.preventDefault(); + self.deactivate(true); + return false; + } + break; + + default: + self.activate(); + + } + }); + + /** + * Attach paste event listener because paste may occur much later then keydown or even without a keydown at all + */ + $elem.on('paste', function() { + self.activate(); + }); + + /** + * Finish on blur event + * Use a timeout because instant blur gives race conditions + */ + var onBlurFunction = function() { + self.deactivate(true); + } + $elem.blur(function() { + if (self.finishOnBlur_) { + self.finishTimeout_ = setTimeout(onBlurFunction, 200); + } + }); + /** + * Catch a race condition on form submit + */ + $elem.parents('form').on('submit', onBlurFunction); + + }; + + /** + * Position output DOM elements + * @private + */ + $.Autocompleter.prototype.position = function() { + var offset = this.dom.$elem.offset(); + var height = this.dom.$results.outerHeight(); + var totalHeight = $(window).outerHeight(); + var inputBottom = offset.top + this.dom.$elem.outerHeight(); + var bottomIfDown = inputBottom + height; + // Set autocomplete results at the bottom of input + var position = {top: inputBottom, left: offset.left}; + if (bottomIfDown > totalHeight) { + // Try to set autocomplete results at the top of input + var topIfUp = offset.top - height; + if (topIfUp >= 0) { + position.top = topIfUp; + } + } + this.dom.$results.css(position); + }; + + /** + * Read from cache + * @private + */ + $.Autocompleter.prototype.cacheRead = function(filter) { + var filterLength, searchLength, search, maxPos, pos; + if (this.options.useCache) { + filter = String(filter); + filterLength = filter.length; + if (this.options.matchSubset) { + searchLength = 1; + } else { + searchLength = filterLength; + } + while (searchLength <= filterLength) { + if (this.options.matchInside) { + maxPos = filterLength - searchLength; + } else { + maxPos = 0; + } + pos = 0; + while (pos <= maxPos) { + search = filter.substr(0, searchLength); + if (this.cacheData_[search] !== undefined) { + return this.cacheData_[search]; + } + pos++; + } + searchLength++; + } + } + return false; + }; + + /** + * Write to cache + * @private + */ + $.Autocompleter.prototype.cacheWrite = function(filter, data) { + if (this.options.useCache) { + if (this.cacheLength_ >= this.options.maxCacheLength) { + this.cacheFlush(); + } + filter = String(filter); + if (this.cacheData_[filter] !== undefined) { + this.cacheLength_++; + } + this.cacheData_[filter] = data; + return this.cacheData_[filter]; + } + return false; + }; + + /** + * Flush cache + * @public + */ + $.Autocompleter.prototype.cacheFlush = function() { + this.cacheData_ = {}; + this.cacheLength_ = 0; + }; + + /** + * Call hook + * Note that all called hooks are passed the autocompleter object + * @param {string} hook + * @param data + * @returns Result of called hook, false if hook is undefined + */ + $.Autocompleter.prototype.callHook = function(hook, data) { + var f = this.options[hook]; + if (f && $.isFunction(f)) { + return f(data, this); + } + return false; + }; + + /** + * Set timeout to activate autocompleter + */ + $.Autocompleter.prototype.activate = function() { + if (!this.options.enabled) return; + var self = this; + if (this.keyTimeout_) { + clearTimeout(this.keyTimeout_); + } + this.keyTimeout_ = setTimeout(function() { + self.activateNow(); + }, this.options.delay); + }; + + /** + * Activate autocompleter immediately + */ + $.Autocompleter.prototype.activateNow = function() { + var value = this.beforeUseConverter(this.dom.$elem.val()); + if (value !== this.lastProcessedValue_ && value !== this.lastSelectedValue_) { + this.fetchData(value); + } + }; + + /** + * Get autocomplete data for a given value + * @param {string} value Value to base autocompletion on + * @private + */ + $.Autocompleter.prototype.fetchData = function(value) { + var self = this; + var processResults = function(results, filter) { + if (self.options.processData) { + results = self.options.processData(results); + } + self.showResults(self.filterResults(results, filter), filter); + }; + this.lastProcessedValue_ = value; + if (value.length < this.options.minChars) { + processResults([], value); + } else if (this.options.data) { + processResults(this.options.data, value); + } else { + this.fetchRemoteData(value, function(remoteData) { + processResults(remoteData, value); + }); + } + }; + + /** + * Get remote autocomplete data for a given value + * @param {string} filter The filter to base remote data on + * @param {function} callback The function to call after data retrieval + * @private + */ + $.Autocompleter.prototype.fetchRemoteData = function(filter, callback) { + var data = this.cacheRead(filter); + if (data) { + callback(data); + } else { + var self = this; + var dataType = self.options.remoteDataType === 'json' ? 'json' : 'text'; + var ajaxCallback = function(data) { + var parsed = false; + if (data !== false) { + parsed = self.parseRemoteData(data); + self.cacheWrite(filter, parsed); + } + self.dom.$elem.removeClass(self.options.loadingClass); + callback(parsed); + }; + this.dom.$elem.addClass(this.options.loadingClass); + $.ajax({ + url: this.makeUrl(filter), + success: ajaxCallback, + error: function(jqXHR, textStatus, errorThrown) { + if($.isFunction(self.options.onError)) { + self.options.onError(jqXHR, textStatus, errorThrown); + } else { + ajaxCallback(false); + } + }, + dataType: dataType + }); + } + }; + + /** + * Create or update an extra parameter for the remote request + * @param {string} name Parameter name + * @param {string} value Parameter value + * @public + */ + $.Autocompleter.prototype.setExtraParam = function(name, value) { + var index = $.trim(String(name)); + if (index) { + if (!this.options.extraParams) { + this.options.extraParams = {}; + } + if (this.options.extraParams[index] !== value) { + this.options.extraParams[index] = value; + this.cacheFlush(); + } + } + + return this; + }; + + /** + * Build the url for a remote request + * If options.queryParamName === false, append query to url instead of using a GET parameter + * @param {string} param The value parameter to pass to the backend + * @returns {string} The finished url with parameters + */ + $.Autocompleter.prototype.makeUrl = function(param) { + var self = this; + var url = this.options.url; + var params = $.extend({}, this.options.extraParams); + + if (this.options.queryParamName === false) { + url += encodeURIComponent(param); + } else { + params[this.options.queryParamName] = param; + } + + return makeUrl(url, params); + }; + + /** + * Parse data received from server + * @param remoteData Data received from remote server + * @returns {array} Parsed data + */ + $.Autocompleter.prototype.parseRemoteData = function(remoteData) { + var remoteDataType; + var data = remoteData; + if (this.options.remoteDataType === 'json') { + remoteDataType = typeof(remoteData); + switch (remoteDataType) { + case 'object': + data = remoteData; + break; + case 'string': + data = $.parseJSON(remoteData); + break; + default: + throw new Error("Unexpected remote data type: " + remoteDataType); + } + return data; + } + return plainTextParser(data, this.options.lineSeparator, this.options.cellSeparator); + }; + + /** + * Default filter for results + * @param {Object} result + * @param {String} filter + * @returns {boolean} Include this result + * @private + */ + $.Autocompleter.prototype.defaultFilter = function(result, filter) { + if (!result.value) { + return false; + } + if (this.options.filterResults) { + var pattern = this.matchStringConverter(filter); + var testValue = this.matchStringConverter(result.value); + if (!this.options.matchCase) { + pattern = pattern.toLowerCase(); + testValue = testValue.toLowerCase(); + } + var patternIndex = testValue.indexOf(pattern); + if (this.options.matchInside) { + return patternIndex > -1; + } else { + return patternIndex === 0; + } + } + return true; + }; + + /** + * Filter result + * @param {Object} result + * @param {String} filter + * @returns {boolean} Include this result + * @private + */ + $.Autocompleter.prototype.filterResult = function(result, filter) { + // No filter + if (this.options.filter === false) { + return true; + } + // Custom filter + if ($.isFunction(this.options.filter)) { + return this.options.filter(result, filter); + } + // Default filter + return this.defaultFilter(result, filter); + }; + + /** + * Filter results + * @param results + * @param filter + */ + $.Autocompleter.prototype.filterResults = function(results, filter) { + var filtered = []; + var i, result; + + for (i = 0; i < results.length; i++) { + result = sanitizeResult(results[i]); + if (this.filterResult(result, filter)) { + filtered.push(result); + } + } + if (this.options.sortResults) { + filtered = this.sortResults(filtered, filter); + } + if (this.options.maxItemsToShow > 0 && this.options.maxItemsToShow < filtered.length) { + filtered.length = this.options.maxItemsToShow; + } + return filtered; + }; + + /** + * Sort results + * @param results + * @param filter + */ + $.Autocompleter.prototype.sortResults = function(results, filter) { + var self = this; + var sortFunction = this.options.sortFunction; + if (!$.isFunction(sortFunction)) { + sortFunction = function(a, b, f) { + return sortValueAlpha(a, b, self.options.matchCase); + }; + } + results.sort(function(a, b) { + return sortFunction(a, b, filter, self.options); + }); + return results; + }; + + /** + * Convert string before matching + * @param s + * @param a + * @param b + */ + $.Autocompleter.prototype.matchStringConverter = function(s, a, b) { + var converter = this.options.matchStringConverter; + if ($.isFunction(converter)) { + s = converter(s, a, b); + } + return s; + }; + + /** + * Convert string before use + * @param {String} s + */ + $.Autocompleter.prototype.beforeUseConverter = function(s) { + s = this.getValue(s); + var converter = this.options.beforeUseConverter; + if ($.isFunction(converter)) { + s = converter(s); + } + return s; + }; + + /** + * Enable finish on blur event + */ + $.Autocompleter.prototype.enableFinishOnBlur = function() { + this.finishOnBlur_ = true; + }; + + /** + * Disable finish on blur event + */ + $.Autocompleter.prototype.disableFinishOnBlur = function() { + this.finishOnBlur_ = false; + }; + + /** + * Create a results item (LI element) from a result + * @param result + */ + $.Autocompleter.prototype.createItemFromResult = function(result) { + var self = this; + var $li = $('
        • '); + $li.html(this.showResult(result.value, result.data)); + $li.data({value: result.value, data: result.data}) + .click(function() { + self.selectItem($li); + }) + .mousedown(self.disableFinishOnBlur) + .mouseup(self.enableFinishOnBlur) + ; + return $li; + }; + + /** + * Get all items from the results list + * @param result + */ + $.Autocompleter.prototype.getItems = function() { + return $('>ul>li', this.dom.$results); + }; + + /** + * Show all results + * @param results + * @param filter + */ + $.Autocompleter.prototype.showResults = function(results, filter) { + var numResults = results.length; + var self = this; + var $ul = $('
            '); + var i, result, $li, autoWidth, first = false, $first = false; + + if (numResults) { + for (i = 0; i < numResults; i++) { + result = results[i]; + $li = this.createItemFromResult(result); + $ul.append($li); + if (first === false) { + first = String(result.value); + $first = $li; + $li.addClass(this.options.firstItemClass); + } + if (i === numResults - 1) { + $li.addClass(this.options.lastItemClass); + } + } + + this.dom.$results.html($ul).show(); + + // Always recalculate position since window size or + // input element location may have changed. + this.position(); + if (this.options.autoWidth) { + autoWidth = this.dom.$elem.outerWidth() - this.dom.$results.outerWidth() + this.dom.$results.width(); + this.dom.$results.css(this.options.autoWidth, autoWidth); + } + this.getItems().hover( + function() { self.focusItem(this); }, + function() { /* void */ } + ); + if (this.autoFill(first, filter) || this.options.selectFirst || (this.options.selectOnly && numResults === 1)) { + this.focusItem($first); + } + this.active_ = true; + } else { + this.hideResults(); + this.active_ = false; + } + }; + + $.Autocompleter.prototype.showResult = function(value, data) { + if ($.isFunction(this.options.showResult)) { + return this.options.showResult(value, data); + } else { + return $('

            ').text(value).html(); + } + }; + + $.Autocompleter.prototype.autoFill = function(value, filter) { + var lcValue, lcFilter, valueLength, filterLength; + if (this.options.autoFill && this.lastKeyPressed_ !== 8) { + lcValue = String(value).toLowerCase(); + lcFilter = String(filter).toLowerCase(); + valueLength = value.length; + filterLength = filter.length; + if (lcValue.substr(0, filterLength) === lcFilter) { + var d = this.getDelimiterOffsets(); + var pad = d.start ? ' ' : ''; // if there is a preceding delimiter + this.setValue( pad + value ); + var start = filterLength + d.start + pad.length; + var end = valueLength + d.start + pad.length; + this.selectRange(start, end); + return true; + } + } + return false; + }; + + $.Autocompleter.prototype.focusNext = function() { + this.focusMove(+1); + }; + + $.Autocompleter.prototype.focusPrev = function() { + this.focusMove(-1); + }; + + $.Autocompleter.prototype.focusMove = function(modifier) { + var $items = this.getItems(); + modifier = sanitizeInteger(modifier, 0); + if (modifier) { + for (var i = 0; i < $items.length; i++) { + if ($($items[i]).hasClass(this.selectClass_)) { + this.focusItem(i + modifier); + return; + } + } + } + this.focusItem(0); + }; + + $.Autocompleter.prototype.focusItem = function(item) { + var $item, $items = this.getItems(); + if ($items.length) { + $items.removeClass(this.selectClass_).removeClass(this.options.selectClass); + if (typeof item === 'number') { + if (item < 0) { + item = 0; + } else if (item >= $items.length) { + item = $items.length - 1; + } + $item = $($items[item]); + } else { + $item = $(item); + } + if ($item) { + $item.addClass(this.selectClass_).addClass(this.options.selectClass); + } + } + }; + + $.Autocompleter.prototype.selectCurrent = function() { + var $item = $('li.' + this.selectClass_, this.dom.$results); + if ($item.length === 1) { + this.selectItem($item); + } else { + this.deactivate(false); + } + }; + + $.Autocompleter.prototype.selectItem = function($li) { + var value = $li.data('value'); + var data = $li.data('data'); + var displayValue = this.displayValue(value, data); + var processedDisplayValue = this.beforeUseConverter(displayValue); + this.lastProcessedValue_ = processedDisplayValue; + this.lastSelectedValue_ = processedDisplayValue; + var d = this.getDelimiterOffsets(); + var delimiter = this.options.delimiterChar; + var elem = this.dom.$elem; + var extraCaretPos = 0; + if ( this.options.useDelimiter ) { + // if there is a preceding delimiter, add a space after the delimiter + if ( elem.val().substring(d.start-1, d.start) == delimiter && delimiter != ' ' ) { + displayValue = ' ' + displayValue; + } + // if there is not already a delimiter trailing this value, add it + if ( elem.val().substring(d.end, d.end+1) != delimiter && this.lastKeyPressed_ != this.options.delimiterKeyCode ) { + displayValue = displayValue + delimiter; + } else { + // move the cursor after the existing trailing delimiter + extraCaretPos = 1; + } + } + this.setValue(displayValue); + this.setCaret(d.start + displayValue.length + extraCaretPos); + this.callHook('onItemSelect', { value: value, data: data }); + this.deactivate(true); + elem.focus(); + }; + + $.Autocompleter.prototype.displayValue = function(value, data) { + if ($.isFunction(this.options.displayValue)) { + return this.options.displayValue(value, data); + } + return value; + }; + + $.Autocompleter.prototype.hideResults = function() { + this.dom.$results.hide(); + }; + + $.Autocompleter.prototype.deactivate = function(finish) { + if (this.finishTimeout_) { + clearTimeout(this.finishTimeout_); + } + if (this.keyTimeout_) { + clearTimeout(this.keyTimeout_); + } + if (finish) { + if (this.lastProcessedValue_ !== this.lastSelectedValue_) { + if (this.options.mustMatch) { + this.setValue(''); + } + this.callHook('onNoMatch'); + } + if (this.active_) { + this.callHook('onFinish'); + } + this.lastKeyPressed_ = null; + this.lastProcessedValue_ = null; + this.lastSelectedValue_ = null; + this.active_ = false; + } + this.hideResults(); + }; + + $.Autocompleter.prototype.selectRange = function(start, end) { + var input = this.dom.$elem.get(0); + if (input.setSelectionRange) { + input.focus(); + input.setSelectionRange(start, end); + } else if (input.createTextRange) { + var range = input.createTextRange(); + range.collapse(true); + range.moveEnd('character', end); + range.moveStart('character', start); + range.select(); + } + }; + + /** + * Move caret to position + * @param {Number} pos + */ + $.Autocompleter.prototype.setCaret = function(pos) { + this.selectRange(pos, pos); + }; + + /** + * Get caret position + */ + $.Autocompleter.prototype.getCaret = function() { + var $elem = this.dom.$elem; + var elem = $elem[0]; + var val, selection, range, start, end, stored_range; + if (elem.createTextRange) { // IE + selection = document.selection; + if (elem.tagName.toLowerCase() != 'textarea') { + val = $elem.val(); + range = selection.createRange().duplicate(); + range.moveEnd('character', val.length); + if (range.text === '') { + start = val.length; + } else { + start = val.lastIndexOf(range.text); + } + range = selection.createRange().duplicate(); + range.moveStart('character', -val.length); + end = range.text.length; + } else { + range = selection.createRange(); + stored_range = range.duplicate(); + stored_range.moveToElementText(elem); + stored_range.setEndPoint('EndToEnd', range); + start = stored_range.text.length - range.text.length; + end = start + range.text.length; + } + } else { + start = $elem[0].selectionStart; + end = $elem[0].selectionEnd; + } + return { + start: start, + end: end + }; + }; + + /** + * Set the value that is currently being autocompleted + * @param {String} value + */ + $.Autocompleter.prototype.setValue = function(value) { + if ( this.options.useDelimiter ) { + // set the substring between the current delimiters + var val = this.dom.$elem.val(); + var d = this.getDelimiterOffsets(); + var preVal = val.substring(0, d.start); + var postVal = val.substring(d.end); + value = preVal + value + postVal; + } + this.dom.$elem.val(value); + }; + + /** + * Get the value currently being autocompleted + * @param {String} value + */ + $.Autocompleter.prototype.getValue = function(value) { + if ( this.options.useDelimiter ) { + var d = this.getDelimiterOffsets(); + return value.substring(d.start, d.end).trim(); + } else { + return value; + } + }; + + /** + * Get the offsets of the value currently being autocompleted + */ + $.Autocompleter.prototype.getDelimiterOffsets = function() { + var val = this.dom.$elem.val(); + if ( this.options.useDelimiter ) { + var preCaretVal = val.substring(0, this.getCaret().start); + var start = preCaretVal.lastIndexOf(this.options.delimiterChar) + 1; + var postCaretVal = val.substring(this.getCaret().start); + var end = postCaretVal.indexOf(this.options.delimiterChar); + if ( end == -1 ) end = val.length; + end += this.getCaret().start; + } else { + start = 0; + end = val.length; + } + return { + start: start, + end: end + }; + }; + +})((typeof window.jQuery == 'undefined' && typeof window.django != 'undefined')? django.jQuery : jQuery); diff --git a/OneCprogsite/static/django_extensions/js/jquery.bgiframe.js b/OneCprogsite/static/django_extensions/js/jquery.bgiframe.js index 1a452ba..d268a17 100644 --- a/OneCprogsite/static/django_extensions/js/jquery.bgiframe.js +++ b/OneCprogsite/static/django_extensions/js/jquery.bgiframe.js @@ -1,39 +1,39 @@ -/*! Copyright (c) 2010 Brandon Aaron (http://brandon.aaron.sh/) - * Licensed under the MIT License (LICENSE.txt). - * - * Version 2.1.2 - */ - -(function($){ - -$.fn.bgiframe = ($.browser.msie && /msie 6\.0/i.test(navigator.userAgent) ? function(s) { - s = $.extend({ - top : 'auto', // auto == .currentStyle.borderTopWidth - left : 'auto', // auto == .currentStyle.borderLeftWidth - width : 'auto', // auto == offsetWidth - height : 'auto', // auto == offsetHeight - opacity : true, - src : 'javascript:false;' - }, s); - var html = '