This commit is contained in:
EDeev 2025-12-06 12:54:15 +03:00
parent 9980762ae6
commit 5f2ebd8792
7 changed files with 525 additions and 264 deletions

View file

@ -1,156 +1,177 @@
import os import os
from pathlib import Path from pathlib import Path
from dotenv import load_dotenv
BASE_DIR = Path(__file__).resolve().parent.parent
load_dotenv()
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', '*****')
DEBUG = os.environ.get('DJANGO_DEBUG', 'False') == 'True' BASE_DIR = Path(__file__).resolve().parent.parent
ALLOWED_HOSTS = ['deev.space', 'deev.su', 'www.deev.space', 'www.deev.su', 'localhost', '127.0.0.1'] SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', '*****')
INSTALLED_APPS = [ DEBUG = os.environ.get('DJANGO_DEBUG', 'False') == 'True'
'django.contrib.admin',
'django.contrib.auth', ALLOWED_HOSTS = ['deev.space', 'deev.su', 'www.deev.space', 'www.deev.su', 'localhost', '127.0.0.1']
'django.contrib.contenttypes',
'django.contrib.sessions', INSTALLED_APPS = [
'django.contrib.messages', 'django.contrib.admin',
'django.contrib.staticfiles', 'django.contrib.auth',
'django.contrib.sitemaps', 'django.contrib.contenttypes',
'main.apps.MainConfig', 'django.contrib.sessions',
] 'django.contrib.messages',
'django.contrib.staticfiles',
MIDDLEWARE = [ 'django.contrib.sitemaps',
'django.middleware.security.SecurityMiddleware', 'main.apps.MainConfig',
'django.contrib.sessions.middleware.SessionMiddleware', ]
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.middleware.security.SecurityMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.common.CommonMiddleware',
] 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
ROOT_URLCONF = 'dspace.urls' 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
TEMPLATES = [ ]
{
'BACKEND': 'django.template.backends.django.DjangoTemplates', ROOT_URLCONF = 'dspace.urls'
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True, TEMPLATES = [
'OPTIONS': { {
'context_processors': [ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
'django.template.context_processors.debug', 'DIRS': [BASE_DIR / 'templates'],
'django.template.context_processors.request', 'APP_DIRS': True,
'django.contrib.auth.context_processors.auth', 'OPTIONS': {
'django.contrib.messages.context_processors.messages', 'context_processors': [
'main.context_processors.site_settings', 'django.template.context_processors.debug',
], 'django.template.context_processors.request',
}, 'django.contrib.auth.context_processors.auth',
}, 'django.contrib.messages.context_processors.messages',
] 'main.context_processors.site_settings',
],
WSGI_APPLICATION = 'dspace.wsgi.application' },
},
DATABASES = { ]
'default': {
'ENGINE': 'django.db.backends.sqlite3', WSGI_APPLICATION = 'dspace.wsgi.application'
'NAME': BASE_DIR / 'db.sqlite3',
} DATABASES = {
} 'default': {
'ENGINE': 'django.db.backends.sqlite3',
AUTH_USER_MODEL = 'main.CustomUser' 'NAME': BASE_DIR / 'db.sqlite3',
}
AUTH_PASSWORD_VALIDATORS = [] }
LANGUAGE_CODE = 'ru-ru' AUTH_USER_MODEL = 'main.CustomUser'
TIME_ZONE = 'Europe/Moscow'
USE_I18N = True AUTH_PASSWORD_VALIDATORS = [
USE_TZ = True {
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
STATIC_URL = '/static/' },
STATICFILES_DIRS = [BASE_DIR / 'static'] {
STATIC_ROOT = BASE_DIR / 'staticfiles' 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
MEDIA_URL = '/media/' {
MEDIA_ROOT = BASE_DIR / 'media' 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' {
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
# Yandex SmartCaptcha Settings },
SMARTCAPTCHA_CLIENT_KEY = os.environ.get('SMARTCAPTCHA_CLIENT_KEY', '*****') ]
SMARTCAPTCHA_SERVER_KEY = os.environ.get('SMARTCAPTCHA_SERVER_KEY', '*****')
LANGUAGE_CODE = 'ru-ru'
# Telegram Bot Settings TIME_ZONE = 'Europe/Moscow'
TELEGRAM_BOT_TOKEN = os.environ.get('TELEGRAM_BOT_TOKEN', '') USE_I18N = True
TELEGRAM_CHANNEL_ID = os.environ.get('TELEGRAM_CHANNEL_ID', '') USE_TZ = True
# VK API Settings STATIC_URL = '/static/'
VK_ACCESS_TOKEN = os.environ.get('VK_ACCESS_TOKEN', '') STATICFILES_DIRS = [BASE_DIR / 'static']
VK_GROUP_ID = os.environ.get('VK_GROUP_ID', '') STATIC_ROOT = BASE_DIR / 'staticfiles'
# Email Settings # Хеширование статических файлов в production
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' if not DEBUG:
EMAIL_HOST = os.environ.get('EMAIL_HOST', 'smtp.yandex.ru') STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
EMAIL_PORT = int(os.environ.get('EMAIL_PORT', 465))
EMAIL_USE_SSL = True MEDIA_URL = '/media/'
EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER', '') MEDIA_ROOT = BASE_DIR / 'media'
EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD', '')
DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', 'egor@deev.space') DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
CONTACT_EMAIL = os.environ.get('CONTACT_EMAIL', 'egor@deev.space')
# Yandex SmartCaptcha Settings
# Site Settings SMARTCAPTCHA_CLIENT_KEY = os.environ.get('SMARTCAPTCHA_CLIENT_KEY', '*****')
SITE_NAME = 'deev.space' SMARTCAPTCHA_SERVER_KEY = os.environ.get('SMARTCAPTCHA_SERVER_KEY', '*****')
SITE_AUTHOR = 'Деев Егор Викторович'
SITE_DESCRIPTION = 'Персональный сайт backend-разработчика' # Telegram Bot Settings
TELEGRAM_BOT_TOKEN = os.environ.get('TELEGRAM_BOT_TOKEN', '')
# Session Settings TELEGRAM_CHANNEL_ID = os.environ.get('TELEGRAM_CHANNEL_ID', '')
SESSION_COOKIE_AGE = 31536000 # 1 year
SESSION_COOKIE_SECURE = not DEBUG # VK API Settings
CSRF_COOKIE_SECURE = not DEBUG VK_ACCESS_TOKEN = os.environ.get('VK_ACCESS_TOKEN', '')
VK_GROUP_ID = os.environ.get('VK_GROUP_ID', '')
# Security Settings (Production)
if not DEBUG: # Email Settings
SECURE_BROWSER_XSS_FILTER = True EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
SECURE_CONTENT_TYPE_NOSNIFF = True EMAIL_HOST = os.environ.get('EMAIL_HOST', 'smtp.yandex.ru')
X_FRAME_OPTIONS = 'DENY' EMAIL_PORT = int(os.environ.get('EMAIL_PORT', 465))
SECURE_SSL_REDIRECT = True EMAIL_USE_SSL = True
SECURE_HSTS_SECONDS = 31536000 EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER', '')
SECURE_HSTS_INCLUDE_SUBDOMAINS = True EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD', '')
SECURE_HSTS_PRELOAD = True DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', 'egor@deev.space')
CONTACT_EMAIL = os.environ.get('CONTACT_EMAIL', 'egor@deev.space')
# Logging
LOGGING = { # Site Settings
'version': 1, SITE_NAME = 'deev.space'
'disable_existing_loggers': False, SITE_AUTHOR = 'Деев Егор Викторович'
'formatters': { SITE_DESCRIPTION = 'Персональный сайт backend-разработчика'
'verbose': {
'format': '{levelname} {asctime} {module} {message}', # Session Settings
'style': '{', SESSION_COOKIE_AGE = 31536000 # 1 year
}, SESSION_COOKIE_SECURE = not DEBUG
}, CSRF_COOKIE_SECURE = not DEBUG
'handlers': {
'file': { # Security Settings (Production)
'level': 'ERROR', if not DEBUG:
'class': 'logging.FileHandler', SECURE_BROWSER_XSS_FILTER = True
'filename': BASE_DIR / 'logs' / 'django.log', SECURE_CONTENT_TYPE_NOSNIFF = True
'formatter': 'verbose', X_FRAME_OPTIONS = 'DENY'
}, SECURE_SSL_REDIRECT = True
'console': { SECURE_HSTS_SECONDS = 31536000
'class': 'logging.StreamHandler', SECURE_HSTS_INCLUDE_SUBDOMAINS = True
'formatter': 'verbose', SECURE_HSTS_PRELOAD = True
},
}, # Logging
'loggers': { LOGGING = {
'django': { 'version': 1,
'handlers': ['console'] if DEBUG else ['file', 'console'], 'disable_existing_loggers': False,
'level': 'INFO', 'formatters': {
}, 'verbose': {
'main': { 'format': '{levelname} {asctime} {module} {message}',
'handlers': ['console'] if DEBUG else ['file', 'console'], 'style': '{',
'level': 'DEBUG' if DEBUG else 'INFO', },
}, },
}, 'handlers': {
} 'file': {
'level': 'ERROR',
# Create logs directory 'class': 'logging.FileHandler',
'filename': BASE_DIR / 'logs' / 'django.log',
'formatter': 'verbose',
},
'console': {
'class': 'logging.StreamHandler',
'formatter': 'verbose',
},
},
'loggers': {
'django': {
'handlers': ['console'] if DEBUG else ['file', 'console'],
'level': 'INFO',
},
'main': {
'handlers': ['console'] if DEBUG else ['file', 'console'],
'level': 'DEBUG' if DEBUG else 'INFO',
},
},
}
# Create logs directory
(BASE_DIR / 'logs').mkdir(exist_ok=True) (BASE_DIR / 'logs').mkdir(exist_ok=True)

View file

@ -399,46 +399,273 @@
} }
.article-body { .article-body {
font-size: 1.1rem; font-size: 1.05rem;
line-height: 1.8; line-height: 1.7;
color: var(--text-secondary); color: var(--text-secondary);
text-align: justify;
text-justify: inter-word;
hyphens: auto;
-webkit-hyphens: auto;
-moz-hyphens: auto;
-ms-hyphens: auto;
} }
.article-body p { .article-body p {
margin-bottom: var(--space-lg); margin-bottom: 1rem;
margin-top: 0;
text-align: justify;
} }
.article-body h2, .article-body h2,
.article-body h3, .article-body h3,
.article-body h4,
.article-body h5,
.article-body h6 {
text-align: left;
}
.article-body h2 {
color: var(--text-primary);
margin-top: 2.5rem;
margin-bottom: 1rem;
font-size: 1.75rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid var(--border-color);
}
.article-body h3 {
color: var(--text-primary);
margin-top: 2rem;
margin-bottom: 0.75rem;
font-size: 1.35rem;
}
.article-body h4 { .article-body h4 {
color: var(--text-primary); color: var(--text-primary);
margin-top: var(--space-2xl); margin-top: 1.5rem;
margin-bottom: var(--space-md); margin-bottom: 0.75rem;
font-size: 1.15rem;
} }
.article-body ul, .article-body ul,
.article-body ol { .article-body ol {
margin-bottom: var(--space-lg); margin-bottom: 1.25rem;
padding-left: var(--space-xl); margin-top: 0.75rem;
padding-left: 2rem;
text-align: justify;
} }
.article-body li { .article-body li {
margin-bottom: var(--space-sm); margin-bottom: 0.5rem;
line-height: 1.6;
text-align: justify;
}
.article-body li p {
margin-bottom: 0.5rem;
} }
.article-body img { .article-body img {
max-width: 100%; max-width: 100%;
height: auto; height: auto;
border-radius: var(--radius-md); border-radius: var(--radius-md);
margin: var(--space-lg) 0; margin: 1.5rem 0;
box-shadow: var(--shadow-md);
} }
.article-body blockquote { .article-body blockquote {
border-left: 4px solid var(--primary-color); border-left: 4px solid var(--primary-color);
padding-left: var(--space-lg); padding: 1rem 1.5rem;
margin: var(--space-lg) 0; margin: 1.5rem 0;
font-style: italic; background: var(--bg-card);
border-radius: var(--radius-sm);
font-style: normal;
color: var(--text-secondary);
text-align: justify;
}
.article-body blockquote p {
text-align: justify;
}
.article-body blockquote p:last-child {
margin-bottom: 0;
}
.article-body hr {
border: none;
height: 1px;
background: var(--border-color);
margin: 2rem 0;
}
.article-body table {
width: 100%;
border-collapse: collapse;
margin: 1.5rem 0;
background: var(--bg-card);
border-radius: var(--radius-md);
overflow: hidden;
}
.article-body th,
.article-body td {
padding: 0.75rem 1rem;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
.article-body th {
background: var(--bg-elevated);
color: var(--text-primary);
font-weight: 600;
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.article-body tr:last-child td {
border-bottom: none;
}
.article-body tr:hover {
background: var(--bg-elevated);
}
.article-body code {
font-family: 'Fira Code', 'Monaco', 'Consolas', monospace;
background: var(--bg-elevated);
padding: 0.2em 0.5em;
border-radius: var(--radius-xs);
font-size: 0.9em;
color: var(--primary-light);
border: 1px solid var(--border-color);
}
.article-body pre {
position: relative;
background: var(--bg-darker);
padding: 1.25rem;
border-radius: var(--radius-md);
overflow-x: auto;
margin: 1.5rem 0;
border: 1px solid var(--border-color);
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.2);
}
.article-body pre code {
background: none;
padding: 0;
border: none;
color: #e0e0e0;
font-size: 0.9rem;
line-height: 1.6;
display: block;
}
.article-body pre:hover .code-copy-btn {
opacity: 1;
}
.code-copy-btn {
position: absolute;
top: 0.75rem;
right: 0.75rem;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
padding: 0.4rem 0.75rem;
font-size: 0.8rem;
color: var(--text-secondary);
cursor: pointer;
transition: var(--transition);
opacity: 0;
display: flex;
align-items: center;
gap: 0.35rem;
}
.code-copy-btn:hover {
background: var(--primary-muted);
border-color: var(--primary-color);
color: var(--primary-color);
}
.code-copy-btn.copied {
background: rgba(34, 197, 94, 0.15);
border-color: var(--success-color);
color: var(--success-color);
}
.code-copy-btn i {
font-size: 0.85rem;
}
.code-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 1rem;
background: var(--bg-elevated);
border-bottom: 1px solid var(--border-color);
border-radius: var(--radius-md) var(--radius-md) 0 0;
margin-bottom: -1px;
}
.code-language {
font-size: 0.75rem;
color: var(--text-muted); color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: 600;
}
.article-body pre.has-header {
border-radius: 0 0 var(--radius-md) var(--radius-md);
margin-top: 0;
}
.article-body h2,
.article-body h3 {
position: relative;
}
.header-anchor {
position: absolute;
left: -1.5rem;
opacity: 0;
transition: opacity 0.2s;
color: var(--text-muted);
text-decoration: none;
font-size: 0.8em;
}
.article-body h2:hover .header-anchor,
.article-body h3:hover .header-anchor {
opacity: 1;
}
.header-anchor:hover {
color: var(--primary-color);
}
.article-body pre,
.article-body pre code {
text-align: left;
hyphens: none;
-webkit-hyphens: none;
}
.article-body table,
.article-body th,
.article-body td {
text-align: left;
}
.article-body figcaption,
.carousel-caption,
.collage-caption {
text-align: center;
} }
/* Article Footer */ /* Article Footer */
@ -689,7 +916,7 @@
gap: var(--space-md); gap: var(--space-md);
} }
.comment { div.comment {
background: var(--bg-card); background: var(--bg-card);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: var(--radius-xl); border-radius: var(--radius-xl);

View file

@ -13,6 +13,8 @@
initBackToTop(); initBackToTop();
initMessages(); initMessages();
initContactForm(); initContactForm();
initCodeBlocks();
addHeaderAnchors();
}); });
// ===== AOS Animation ===== // ===== AOS Animation =====
@ -453,4 +455,98 @@
}); });
}; };
// ===== Code Copy Functionality =====
function initCodeBlocks() {
const codeBlocks = document.querySelectorAll('.article-body pre');
codeBlocks.forEach((pre) => {
// Проверяем, не добавлена ли уже кнопка
if (pre.querySelector('.code-copy-btn')) {
return;
}
const codeElement = pre.querySelector('code');
if (!codeElement) return;
// Создаём кнопку копирования
const copyButton = document.createElement('button');
copyButton.className = 'code-copy-btn';
copyButton.type = 'button';
copyButton.setAttribute('aria-label', 'Копировать код');
copyButton.innerHTML = '<i class="fas fa-copy"></i><span class="copy-text">Копировать</span>';
// Обработчик клика
copyButton.addEventListener('click', async function(e) {
e.preventDefault();
// Получаем текст кода без HTML-тегов
const codeText = codeElement.textContent || codeElement.innerText;
try {
await navigator.clipboard.writeText(codeText);
// Изменяем визуальное состояние кнопки
copyButton.innerHTML = '<i class="fas fa-check"></i><span class="copy-text">Сохранено!</span>';
copyButton.classList.add('copied');
// Возвращаем исходное состояние через 2 секунды
setTimeout(() => {
copyButton.innerHTML = '<i class="fas fa-copy"></i><span class="copy-text">Копировать</span>';
copyButton.classList.remove('copied');
}, 2000);
} catch (err) {
console.error('Ошибка при копировании:', err);
// Fallback для старых браузеров
const textArea = document.createElement('textarea');
textArea.value = codeText;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
copyButton.innerHTML = '<i class="fas fa-check"></i><span class="copy-text">Сохранено!</span>';
copyButton.classList.add('copied');
setTimeout(() => {
copyButton.innerHTML = '<i class="fas fa-copy"></i><span class="copy-text">Копировать</span>';
copyButton.classList.remove('copied');
}, 2000);
} catch (err2) {
showNotification('Не удалось скопировать код', 'error');
}
document.body.removeChild(textArea);
}
});
// Добавляем кнопку в блок кода
pre.style.position = 'relative';
pre.appendChild(copyButton);
});
}
function addHeaderAnchors() {
const headers = document.querySelectorAll('.article-body h2, .article-body h3');
headers.forEach(header => {
const id = header.textContent
.toLowerCase()
.replace(/[^a-zа-яё0-9]+/g, '-')
.replace(/^-|-$/g, '');
header.id = id;
const anchor = document.createElement('a');
anchor.className = 'header-anchor';
anchor.href = `#${id}`;
anchor.innerHTML = '<i class="fas fa-link"></i>';
anchor.setAttribute('aria-label', 'Ссылка на раздел');
header.appendChild(anchor);
});
}
})(); })();

View file

@ -6,9 +6,7 @@
<div class="auth-card" data-aos="fade-up"> <div class="auth-card" data-aos="fade-up">
<div class="auth-header"> <div class="auth-header">
<a href="{% url 'index' %}" class="logo"> <a href="{% url 'index' %}" class="logo">
<span class="logo-deev">deev</span> <img src="{% static 'img/logo.png' %}" alt="deev.space" style="max-width: 130px; height: auto;">
<span class="logo-dot"></span>
<span class="logo-space">space</span>
</a> </a>
<h1 class="auth-title">Вход в аккаунт</h1> <h1 class="auth-title">Вход в аккаунт</h1>
<p class="auth-subtitle">Войдите, чтобы оставлять комментарии и оценивать статьи</p> <p class="auth-subtitle">Войдите, чтобы оставлять комментарии и оценивать статьи</p>

View file

@ -6,9 +6,7 @@
<div class="auth-card" data-aos="fade-up"> <div class="auth-card" data-aos="fade-up">
<div class="auth-header"> <div class="auth-header">
<a href="{% url 'index' %}" class="logo"> <a href="{% url 'index' %}" class="logo">
<span class="logo-deev">deev</span> <img src="{% static 'img/logo.png' %}" alt="deev.space" style="max-width: 130px; height: auto;">
<span class="logo-dot"></span>
<span class="logo-space">space</span>
</a> </a>
<h1 class="auth-title">Регистрация</h1> <h1 class="auth-title">Регистрация</h1>
<p class="auth-subtitle">Создайте аккаунт для участия в обсуждениях</p> <p class="auth-subtitle">Создайте аккаунт для участия в обсуждениях</p>

View file

@ -91,7 +91,7 @@
<!-- Article Content --> <!-- Article Content -->
<div class="article-body" data-aos="fade-up" data-aos-delay="300"> <div class="article-body" data-aos="fade-up" data-aos-delay="300">
{{ article.post|safe|linebreaks }} {{ article.post|safe }}
</div> </div>
<!-- Image Gallery --> <!-- Image Gallery -->

View file

@ -24,7 +24,6 @@
<meta property="og:description" content="{{ page_description|default:global_settings.site_description }}"> <meta property="og:description" content="{{ page_description|default:global_settings.site_description }}">
<meta property="og:type" content="{% block og_type %}website{% endblock %}"> <meta property="og:type" content="{% block og_type %}website{% endblock %}">
<meta property="og:url" content="https://deev.space{{ request.path }}"> <meta property="og:url" content="https://deev.space{{ request.path }}">
<meta property="og:image" content="{% block og_image %}{% static 'images/og-default.jpg' %}{% endblock %}">
<meta property="og:site_name" content="{{ global_settings.site_name }}"> <meta property="og:site_name" content="{{ global_settings.site_name }}">
<meta property="og:locale" content="ru_RU"> <meta property="og:locale" content="ru_RU">
@ -32,7 +31,6 @@
<meta name="twitter:card" content="summary_large_image"> <meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{{ page_title|default:global_settings.owner_name }}"> <meta name="twitter:title" content="{{ page_title|default:global_settings.owner_name }}">
<meta name="twitter:description" content="{{ page_description|default:global_settings.site_description }}"> <meta name="twitter:description" content="{{ page_description|default:global_settings.site_description }}">
<meta name="twitter:image" content="{% block twitter_image %}{% static 'images/og-default.jpg' %}{% endblock %}">
<!-- Canonical URL --> <!-- Canonical URL -->
<link rel="canonical" href="https://deev.space{{ request.path }}"> <link rel="canonical" href="https://deev.space{{ request.path }}">
@ -55,6 +53,10 @@
<!-- AOS Animation --> <!-- AOS Animation -->
<link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet"> <link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet">
<!-- Prism.js для подсветки синтаксиса -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/line-numbers/prism-line-numbers.min.css">
<!-- Custom Styles --> <!-- Custom Styles -->
<link rel="stylesheet" href="{% static 'css/variables.css' %}"> <link rel="stylesheet" href="{% static 'css/variables.css' %}">
@ -255,93 +257,6 @@
</div> </div>
</div> </div>
</footer> </footer>
<!-- Footer -->
<!-- <footer class="footer">
<div class="container">
<div class="footer-grid">
<div class="footer-brand">
<a href="{% url 'index' %}" class="logo">
</a>
<p>{{ global_settings.site_description|default:"Персональный сайт backend-разработчика. Создаю надёжные и масштабируемые решения на Python и Django." }}</p>
</div>
<div class="footer-nav">
<h4>Навигация</h4>
<ul class="footer-links">
<li><a href="{% url 'about' %}">Обо мне</a></li>
<li><a href="{% url 'projects' %}">Проекты</a></li>
<li><a href="{% url 'blog' %}">Блог</a></li>
<li><a href="{% url 'achievements' %}">Достижения</a></li>
<li><a href="{% url 'contacts' %}">Контакты</a></li>
</ul>
</div>
<div class="footer-contact">
<h4>Контакты</h4>
<ul class="footer-links">
<li>
<a href="mailto:{{ global_settings.owner_email }}">
<i class="fas fa-envelope"></i>
{{ global_settings.owner_email }}
</a>
</li>
{% if global_settings.owner_phone %}
<li>
<a href="tel:{{ global_settings.owner_phone|cut:' '|cut:'('|cut:')'|cut:'-' }}">
<i class="fas fa-phone"></i>
{{ global_settings.owner_phone }}
</a>
</li>
{% endif %}
<li>
<i class="fas fa-map-marker-alt"></i>
{{ global_settings.owner_city }}, Россия
</li>
</ul>
</div>
<div class="footer-social">
<h4>Социальные сети</h4>
<div class="footer-socials">
{% if global_settings.telegram_url %}
<a href="{{ global_settings.telegram_url }}" target="_blank" rel="noopener noreferrer" class="footer-social-link" aria-label="Telegram">
<i class="fab fa-telegram"></i>
</a>
{% endif %}
{% if global_settings.github_url %}
<a href="{{ global_settings.github_url }}" target="_blank" rel="noopener noreferrer" class="footer-social-link" aria-label="GitHub">
<i class="fab fa-github"></i>
</a>
{% endif %}
{% if global_settings.vk_url %}
<a href="{{ global_settings.vk_url }}" target="_blank" rel="noopener noreferrer" class="footer-social-link" aria-label="VKontakte">
<i class="fab fa-vk"></i>
</a>
{% endif %}
{% if global_settings.linkedin_url %}
<a href="{{ global_settings.linkedin_url }}" target="_blank" rel="noopener noreferrer" class="footer-social-link" aria-label="LinkedIn">
<i class="fab fa-linkedin"></i>
</a>
{% endif %}
<a href="mailto:{{ global_settings.owner_email }}" class="footer-social-link" aria-label="Email">
<i class="fas fa-envelope"></i>
</a>
</div>
</div>
</div>
<div class="footer-bottom">
<p class="footer-copyright">
&copy; 2024-{% now "Y" %} {{ global_settings.owner_name }}. Все права защищены.
</p>
<p class="footer-made">
Сделано с <i class="fas fa-heart" style="color: var(--primary-color);"></i> на Django
</p>
</div>
</div>
</footer> -->
<!-- Back to Top --> <!-- Back to Top -->
<button class="back-to-top" id="backToTop" aria-label="Наверх"> <button class="back-to-top" id="backToTop" aria-label="Наверх">
@ -351,6 +266,12 @@
<!-- Scripts --> <!-- Scripts -->
<script src="https://unpkg.com/aos@2.3.1/dist/aos.js"></script> <script src="https://unpkg.com/aos@2.3.1/dist/aos.js"></script>
<script src="{% static 'js/main.js' %}"></script> <script src="{% static 'js/main.js' %}"></script>
<!-- Prism.js скрипты -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/line-numbers/prism-line-numbers.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/normalize-whitespace/prism-normalize-whitespace.min.js"></script>
{% block extra_js %}{% endblock %} {% block extra_js %}{% endblock %}
</body> </body>