← Назад к шаблонам На главную
Анимации

Живой терминал проекта

Интерактивный блок с терминалом, кнопками сценариев и анимацией печати команд. Подходит для портфолио backend-разработчика.

Сложность: Средняя HTML CSS JavaScript typing effect terminal UI интерактив
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);
🚀 Рассчитать проект