Анимации
Живой терминал проекта
Интерактивный блок с терминалом, кнопками сценариев и анимацией печати команд. Подходит для портфолио backend-разработчика.
m0r64n4 / deploy lab
Живой терминал проекта
Интерактивный блок для IT-сайтов и портфолио. Позволяет наглядно показать процесс работы, этапы развёртывания проекта или технические возможности продукта. Повышает вовлечённость и делает сайт более запоминающимся.
ubuntu@portfolio-django
<section class="terminal-showcase">
<div class="terminal-showcase__content">
<span class="terminal-showcase__badge">m0r64n4 / deploy lab</span>
<h2>Живой терминал проекта</h2>
<p>
Интерактивный блок для IT-сайтов и портфолио.
Позволяет наглядно показать процесс работы, этапы развёртывания проекта или технические возможности продукта.
Повышает вовлечённость и делает сайт более запоминающимся.
</p>
<div class="terminal-showcase__actions">
<button class="terminal-action active" data-script="deploy">Deploy</button>
<button class="terminal-action" data-script="check">Check</button>
<button class="terminal-action" data-script="logs">Logs</button>
</div>
</div>
<div class="terminal-window">
<div class="terminal-window__top">
<span></span>
<span></span>
<span></span>
<strong>ubuntu@portfolio-django</strong>
</div>
<pre class="terminal-output" aria-live="polite"></pre>
</div>
</section>
.terminal-showcase {
position: relative;
display: grid;
grid-template-columns: 0.9fr 1.1fr;
gap: 28px;
align-items: center;
min-height: 480px;
padding: 46px;
overflow: hidden;
border-radius: 28px;
background:
radial-gradient(circle at 20% 20%, rgba(59, 130, 246, 0.32), transparent 34%),
radial-gradient(circle at 80% 90%, rgba(34, 197, 94, 0.18), transparent 32%),
linear-gradient(135deg, #020617, #0f172a 55%, #111827);
}
.terminal-showcase::before {
content: "";
position: absolute;
inset: 24px;
border: 1px solid rgba(148, 163, 184, 0.16);
border-radius: 24px;
pointer-events: none;
}
.terminal-showcase__content,
.terminal-window {
position: relative;
z-index: 2;
}
.terminal-showcase__badge {
display: inline-flex;
margin-bottom: 18px;
padding: 8px 13px;
border-radius: 999px;
color: #bbf7d0;
background: rgba(34, 197, 94, 0.12);
border: 1px solid rgba(34, 197, 94, 0.35);
font-size: 13px;
}
.terminal-showcase h2 {
margin: 0 0 18px;
color: #ffffff;
font-size: clamp(32px, 5vw, 56px);
line-height: 1;
letter-spacing: -0.04em;
}
.terminal-showcase p {
max-width: 480px;
margin-bottom: 24px;
color: #cbd5e1;
font-size: 17px;
line-height: 1.7;
}
.terminal-showcase__actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.terminal-action {
border: 1px solid rgba(148, 163, 184, 0.35);
border-radius: 999px;
padding: 10px 15px;
color: #cbd5e1;
background: rgba(15, 23, 42, 0.72);
cursor: pointer;
transition: 0.25s ease;
}
.terminal-action:hover,
.terminal-action.active {
color: #ffffff;
border-color: #22c55e;
background: rgba(34, 197, 94, 0.16);
transform: translateY(-2px);
}
.terminal-window {
overflow: hidden;
border-radius: 22px;
background: rgba(2, 6, 23, 0.86);
border: 1px solid rgba(96, 165, 250, 0.28);
box-shadow: 0 30px 90px rgba(2, 6, 23, 0.55);
}
.terminal-window__top {
display: flex;
align-items: center;
gap: 8px;
padding: 14px 16px;
border-bottom: 1px solid rgba(51, 65, 85, 0.9);
background: rgba(15, 23, 42, 0.9);
}
.terminal-window__top span {
width: 11px;
height: 11px;
border-radius: 50%;
}
.terminal-window__top span:nth-child(1) {
background: #ef4444;
}
.terminal-window__top span:nth-child(2) {
background: #f59e0b;
}
.terminal-window__top span:nth-child(3) {
background: #22c55e;
}
.terminal-window__top strong {
margin-left: 8px;
color: #94a3b8;
font-size: 13px;
font-weight: 600;
}
.terminal-output {
min-height: 300px;
margin: 0;
padding: 22px;
color: #dbeafe;
font-size: 15px;
line-height: 1.75;
white-space: pre-wrap;
}
.terminal-output::after {
content: "█";
color: #22c55e;
animation: terminalBlink 0.9s steps(2, start) infinite;
}
@keyframes terminalBlink {
50% {
opacity: 0;
}
}
@media (max-width: 768px) {
.terminal-showcase {
grid-template-columns: 1fr;
padding: 28px 20px;
}
.terminal-output {
font-size: 13px;
}
}
const terminalScripts = {
deploy: [
"$ git pull origin main",
"Already up to date.",
"$ python manage.py migrate",
"Operations to perform: Apply all migrations",
"No migrations to apply.",
"$ python manage.py collectstatic --noinput",
"128 static files copied.",
"$ sudo systemctl restart portfolio-django",
"portfolio-django.service restarted successfully."
],
check: [
"$ python manage.py check",
"System check identified no issues.",
"$ sudo nginx -t",
"nginx: configuration file syntax is ok",
"nginx: configuration file test is successful.",
"$ curl -I https://m0r64n4.ru",
"HTTP/2 200"
],
logs: [
"$ journalctl -u portfolio-django -n 5",
"Started Gunicorn for portfolio Django project.",
"Listening at: http://127.0.0.1:8000",
"Booting worker with pid: 1842",
"Booting worker with pid: 1843",
"Request completed: 200 OK"
]
};
const output = document.querySelector(".terminal-output");
const buttons = document.querySelectorAll(".terminal-action");
let typingTimer = null;
function typeTerminal(lines) {
clearTimeout(typingTimer);
output.textContent = "";
const text = lines.join("\n");
let index = 0;
function typeNextChar() {
output.textContent = text.slice(0, index);
index += 1;
if (index <= text.length) {
typingTimer = setTimeout(typeNextChar, 18);
}
}
typeNextChar();
}
buttons.forEach(function (button) {
button.addEventListener("click", function () {
buttons.forEach(function (item) {
item.classList.remove("active");
});
button.classList.add("active");
typeTerminal(terminalScripts[button.dataset.script]);
});
});
typeTerminal(terminalScripts.deploy);