This commit is contained in:
coma 2025-09-25 01:54:30 +04:00
commit 5ab2d8abfd
45 changed files with 9738 additions and 0 deletions

View file

@ -0,0 +1,72 @@
<script>
import GlitchText from './GlitchText.svelte';
import CRTScreen from './CRTScreen.svelte';
import { formatDate } from '../utils/date.js';
export let post = {
slug: '',
title: '',
excerpt: '',
date: '',
author: ''
};
</script>
<CRTScreen class="blog-card">
<div class="card-content">
<h3 class="card-title">
<GlitchText text={post.title} size="1.2rem" />
</h3>
<p class="card-excerpt">{post.excerpt}</p>
<div class="card-meta">
<span class="card-date">{formatDate(post.date)}</span>
<span class="card-author">by {post.author}</span>
</div>
<div class="card-actions">
<a href={`/blog/${post.slug}`} class="read-more">Читать →</a>
</div>
</div>
</CRTScreen>
<style>
.blog-card {
margin-bottom: 20px;
transition: transform 0.3s ease;
}
.blog-card:hover {
transform: translateY(-2px);
}
.card-title {
margin: 0 0 10px 0;
color: #33ff00;
}
.card-excerpt {
color: #ccc;
line-height: 1.4;
margin-bottom: 15px;
}
.card-meta {
display: flex;
justify-content: space-between;
font-size: 0.9rem;
color: #888;
margin-bottom: 15px;
}
.read-more {
color: #ff00ff;
text-decoration: none;
font-weight: bold;
}
.read-more:hover {
text-decoration: underline;
}
</style>

View file

@ -0,0 +1,76 @@
<script>
export let scanlines = true;
export let flicker = true;
</script>
<div class="crt-screen" class:no-scanlines={!scanlines} class:no-flicker={!flicker}>
<div class="content">
<slot />
</div>
<div class="scanlines"></div>
<div class="glow"></div>
</div>
<style>
.crt-screen {
position: relative;
padding: 20px;
overflow: hidden;
box-shadow: 0 0 20px rgba(51, 255, 0, 0.3);
width: 100%;
height: 100%;
}
.content {
position: relative;
z-index: 2;
}
.scanlines {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(
to bottom,
rgba(255, 255, 255, 0.03) 50%,
rgba(0, 0, 0, 0.1) 50%
);
background-size: 100% 4px;
z-index: 1;
pointer-events: none;
}
.glow {
position: absolute;
top: -10px;
left: -10px;
right: -10px;
bottom: -10px;
background: radial-gradient(
circle at center,
rgba(51, 255, 0, 0.1) 0%,
transparent 70%
);
z-index: 0;
pointer-events: none;
}
.no-scanlines .scanlines {
display: none;
}
.no-flicker {
animation: flicker 0.15s infinite;
}
@keyframes flicker {
0% { opacity: 0.9; }
5% { opacity: 0.8; }
10% { opacity: 0.9; }
15% { opacity: 1; }
20% { opacity: 0.9; }
100% { opacity: 1; }
}
</style>

View file

@ -0,0 +1,7 @@
<script>
import { Alert } from '@sveltestrap/sveltestrap';
</script>
<Alert color="danger" class="my-3">
{message}
</Alert>

View file

@ -0,0 +1,104 @@
<script>
import { onMount } from 'svelte';
import { writable } from 'svelte/store';
const systemStatus = writable('OPERATIONAL');
const uptime = writable('0d 0h 0m');
onMount(() => {
const startTime = Date.now();
function updateUptime() {
const diff = Date.now() - startTime;
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
uptime.set(`${days}d ${hours}h ${minutes}m`);
}
updateUptime();
const interval = setInterval(updateUptime, 60000);
return () => clearInterval(interval);
});
</script>
<footer class="minimal-footer">
<div class="footer-container">
<div class="footer-left">
<span class="footer-text">[58] Team © 2024</span>
<span class="footer-separator">|</span>
<span class="footer-status">
<span class="status-indicator"></span>
{$systemStatus}
</span>
</div>
<div class="footer-right">
<span class="footer-uptime">UPTIME: {$uptime}</span>
<span class="footer-separator">|</span>
<span class="footer-version">v1.0.0</span>
</div>
</div>
</footer>
<style>
.minimal-footer {
background: rgba(10, 10, 10, 0.95);
border-top: 1px solid var(--terminal-border);
padding: 8px 20px;
font-family: 'JetBrains Mono', 'Fira Code', 'Courier New', monospace;
font-size: 0.8rem;
}
.footer-container {
display: flex;
justify-content: space-between;
align-items: center;
max-width: 100%;
}
.footer-left, .footer-right {
display: flex;
align-items: center;
gap: 10px;
color: var(--text-muted);
}
.footer-separator {
color: var(--terminal-border);
}
.status-indicator {
display: inline-block;
width: 6px;
height: 6px;
background: var(--primary-green);
border-radius: 50%;
margin-right: 6px;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* Адаптивность */
@media (max-width: 768px) {
.minimal-footer {
padding: 6px 10px;
font-size: 0.7rem;
}
.footer-container {
flex-direction: column;
gap: 5px;
text-align: center;
}
.footer-left, .footer-right {
gap: 8px;
}
}
</style>

View file

@ -0,0 +1,58 @@
<script>
export let text = '';
export let glitch = true;
export let size = '1rem';
</script>
<div class="glitch-text" class:no-glitch={!glitch} style="font-size: {size}">
<span class="glitch" data-text="{text}">{text}</span>
<span class="glitch glitch-2" data-text="{text}">{text}</span>
<span class="glitch glitch-3" data-text="{text}">{text}</span>
</div>
<style>
.glitch-text {
position: relative;
display: inline-block;
}
.glitch {
position: relative;
color: #fff;
text-shadow:
0.05em 0 0 rgba(255, 0, 0, 0.75),
-0.05em -0.025em 0 rgba(0, 255, 0, 0.75),
0.025em 0.05em 0 rgba(0, 0, 255, 0.75);
animation: glitch 2s infinite;
}
.glitch-2 {
animation: glitch-2 3s infinite;
opacity: 0.8;
position: absolute;
top: 0;
left: 0;
}
.glitch-3 {
animation: glitch-3 4s infinite;
opacity: 0.6;
position: absolute;
top: 0;
left: 0;
}
@keyframes glitch {
0% { transform: translate(0); }
20% { transform: translate(-2px, 2px); }
40% { transform: translate(-2px, -2px); }
60% { transform: translate(2px, 2px); }
80% { transform: translate(2px, -2px); }
100% { transform: translate(0); }
}
.no-glitch .glitch {
animation: none;
text-shadow: none;
}
</style>

View file

@ -0,0 +1,7 @@
<script>
import { Spinner } from '@sveltestrap/sveltestrap';
</script>
<div class="text-center my-5">
<Spinner color="primary" />
</div>

View file

@ -0,0 +1,9 @@
<script>
import { marked } from 'marked';
import sanitizeHtml from 'sanitize-html';
export let content = '';
$: html = sanitizeHtml(marked.parse(content || ''));
</script>
<div class="prose lg:prose-xl" bind:this={el} {@html html}></div>

View file

@ -0,0 +1,257 @@
<script>
import { onMount } from 'svelte';
import { writable } from 'svelte/store';
const currentPath = writable('/');
const currentLanguage = writable('EN');
const isMobile = writable(false);
const paths = [
{ href: '/', label: '~', icon: '🏠' },
{ href: '/blog', label: 'blog', icon: '📝' },
{ href: '/team', label: 'team', icon: '👥' },
{ href: '/about', label: 'about', icon: '' },
];
const languages = ['EN', 'RU', 'DE'];
onMount(() => {
isMobile.set(window.innerWidth < 768);
const handleResize = () => isMobile.set(window.innerWidth < 768);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
});
function setLanguage(lang) {
currentLanguage.set(lang);
}
onMount(() => {
function updateTime() {
const timeElement = document.getElementById('current-time');
if (timeElement) {
timeElement.textContent = new Date().toLocaleTimeString('en-US', {
hour12: false,
hour: '2-digit',
minute: '2-digit'
});
}
}
updateTime();
const interval = setInterval(updateTime, 1000);
return () => clearInterval(interval);
});
</script>
<header class="waybar-header">
<div class="waybar-container">
<!-- Левая часть - Пути -->
<nav class="path-navigation">
{#each paths as path}
<a
href={path.href}
class:active={$currentPath === path.href}
class="path-item"
>
<span class="path-icon">{path.icon}</span>
<span class="path-label">{path.label}</span>
</a>
{/each}
</nav>
<!-- Центр - Логотип -->
<div class="waybar-center">
<div class="logo-container">
<span class="logo-brackets">[</span>
<span class="logo-number">58</span>
<span class="logo-brackets">]</span>
<span class="logo-text">Team</span>
</div>
</div>
<!-- Правая часть - Язык и время -->
<div class="waybar-right">
<div class="language-selector">
<button
class="language-btn"
on:click={() => setLanguage(languages[(languages.indexOf($currentLanguage) + 1) % languages.length])}
>
<span class="language-flag">🌐</span>
<span class="language-code">{$currentLanguage}</span>
</button>
</div>
<div class="time-display">
<span class="time" id="current-time">
{new Date().toLocaleTimeString('en-US', { hour12: false })}
</span>
</div>
</div>
</div>
</header>
<style>
.waybar-header {
background: rgba(10, 10, 10, 0.95);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--primary-green);
height: 50px;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
font-family: 'JetBrains Mono', 'Fira Code', 'Courier New', monospace;
}
.waybar-container {
display: flex;
align-items: center;
justify-content: space-between;
height: 100%;
padding: 0 20px;
max-width: 100%;
}
.path-navigation {
display: flex;
gap: 2px;
}
.path-item {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
color: var(--text-secondary);
text-decoration: none;
border-radius: 6px;
transition: all 0.3s ease;
font-size: 0.9rem;
border: 1px solid transparent;
}
.path-item:hover {
background: rgba(51, 255, 0, 0.1);
color: var(--primary-green);
border-color: var(--primary-green);
}
/*.path-item.active {
background: var(--primary-green);
color: var(--bg-dark);
font-weight: bold;
}*/
.path-icon {
font-size: 0.8rem;
}
.waybar-center {
position: absolute;
left: 50%;
transform: translateX(-50%);
}
.logo-container {
display: flex;
align-items: center;
gap: 2px;
font-weight: bold;
font-size: 1.1rem;
}
.logo-brackets {
color: var(--secondary-pink);
}
.logo-number {
color: var(--primary-green);
}
.logo-text {
color: var(--text-secondary);
margin-left: 4px;
}
.waybar-right {
display: flex;
align-items: center;
gap: 15px;
}
.language-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
background: rgba(51, 255, 0, 0.1);
border: 1px solid var(--primary-green);
border-radius: 6px;
color: var(--primary-green);
cursor: pointer;
transition: all 0.3s ease;
font-family: inherit;
}
.language-btn:hover {
background: var(--primary-green);
color: var(--bg-dark);
}
.time-display {
color: var(--text-secondary);
font-size: 0.9rem;
padding: 4px 8px;
background: rgba(0, 0, 0, 0.3);
border-radius: 4px;
border: 1px solid var(--terminal-border);
}
/* Адаптивность */
@media (max-width: 768px) {
.waybar-container {
padding: 0 10px;
}
.path-label {
display: none;
}
.path-item {
padding: 8px;
}
.logo-container {
font-size: 1rem;
}
.language-code {
display: none;
}
.time {
font-size: 0.8rem;
}
}
@media (max-width: 480px) {
.waybar-header {
height: 40px;
}
.path-item {
padding: 6px;
}
.logo-container {
font-size: 0.9rem;
}
.waybar-right {
gap: 8px;
}
}
</style>

View file

@ -0,0 +1,85 @@
<script>
export let href = '';
export let type = 'button';
export let variant = 'primary';
export let disabled = false;
</script>
{#if href}
<a href={href} class="retro-button {variant}" class:disabled>
<span class="button-text"><slot /></span>
<span class="button-border"></span>
</a>
{:else}
<button type={type} class="retro-button {variant}" class:disabled {disabled}>
<span class="button-text"><slot /></span>
<span class="button-border"></span>
</button>
{/if}
<style>
.retro-button {
position: relative;
display: inline-block;
padding: 12px 24px;
background: #000;
color: #33ff00;
border: none;
font-family: 'Courier New', monospace;
font-size: 16px;
text-transform: uppercase;
text-decoration: none;
cursor: pointer;
transition: all 0.3s ease;
}
.button-text {
position: relative;
z-index: 2;
}
.button-border {
position: absolute;
top: 4px;
left: 4px;
right: -4px;
bottom: -4px;
border: 2px solid #33ff00;
z-index: 1;
transition: all 0.3s ease;
}
.retro-button:hover .button-border {
top: -2px;
left: -2px;
right: 2px;
bottom: 2px;
}
.retro-button:active .button-border {
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.retro-button.secondary {
color: #ff00ff;
}
.retro-button.secondary .button-border {
border-color: #ff00ff;
}
.retro-button.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.retro-button.disabled:hover .button-border {
top: 4px;
left: 4px;
right: -4px;
bottom: -4px;
}
</style>

View file

@ -0,0 +1,84 @@
<script>
import BlogCard from '$lib/components/BlogCard.svelte';
import GlitchText from '$lib/components/GlitchText.svelte';
import CRTScreen from '$lib/components/CRTScreen.svelte';
export let blogPosts = [];
</script>
<section class="blog-section">
<div class="section-title">
<GlitchText text="ПОСЛЕДНИЕ СТАТЬИ" size="2rem" />
</div>
<div class="blog-container">
<CRTScreen class="blog-terminal">
<div class="terminal-header">
<span class="blink"></span> LATEST BLOG POSTS
</div>
<div class="posts-grid">
{#each blogPosts as post}
<BlogCard {post} />
{/each}
</div>
<div class="terminal-footer">
<a href="/blog" class="view-all">Просмотреть все статьи →</a>
</div>
</CRTScreen>
</div>
</section>
<style>
.blog-section {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px;
background: #000;
}
.section-title {
margin-bottom: 40px;
}
.blog-container {
width: 100%;
max-width: 1200px;
height: 70vh;
}
.blog-terminal {
height: 100%;
display: flex;
flex-direction: column;
}
.posts-grid {
flex: 1;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
padding: 20px;
overflow-y: auto;
}
.terminal-footer {
padding: 20px;
border-top: 1px solid #333;
text-align: center;
}
.view-all {
color: #ff00ff;
text-decoration: none;
font-size: 1.1rem;
font-weight: bold;
}
.view-all:hover {
text-decoration: underline;
}
</style>

View file

@ -0,0 +1,557 @@
<script>
import { onMount } from 'svelte';
import { writable } from 'svelte/store';
import { Container, Row, Col } from '@sveltestrap/sveltestrap';
import GlitchText from '$lib/components/GlitchText.svelte';
import CRTScreen from '$lib/components/CRTScreen.svelte';
const slogans = [
"CODE LIKE IT'S 1999",
"RETRO FUTURE DEVELOPMENT",
"GLITCH IS A FEATURE",
"VHS MEETS AI",
"OLD SCHOOL COOL",
"BYTE THE DUST"
];
const currentSlogan = writable(slogans[0]);
const logLines = writable([]);
const systemStats = writable({});
const fileSystemLines = writable([]);
// ASCII логотип команды
const asciiLogo = `
███████╗██████╗ ████████╗███████╗ █████╗ ███╗ ███╗
██╔════╝██╔══██╗ ╚══██╔══╝██╔════╝██╔══██╗████╗ ████║
█████╗ ██████╔╝ ██║ █████╗ ███████║██╔████╔██║
██╔══╝ ██╔══██╗ ██║ ██╔══╝ ██╔══██║██║╚██╔╝██║
██║ ██║ ██║ ██║ ███████╗██║ ██║██║ ╚═╝ ██║
╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝
`.trim();
let sloganInterval;
let logInterval;
let statsInterval;
let fileSystemInterval;
onMount(() => {
// Смена слоганов
let sloganIndex = 0;
sloganInterval = setInterval(() => {
sloganIndex = (sloganIndex + 1) % slogans.length;
currentSlogan.set(slogans[sloganIndex]);
}, 3000);
// Генерация системных логов
const logMessages = [
"kernel: Initializing retro computing module...",
"systemd: Started VHS Effect Service",
"network: eth0: link up (1000 Mbps)",
"cpu: Frequency scaled to 3.8 GHz",
"memory: Allocating 2GB for glitch buffer",
"storage: SSD read: 3500 MB/s",
"gpu: Rendering 90s effects [OK]",
"audio: PCM 44100Hz stereo initialized"
];
logInterval = setInterval(() => {
const randomLog = logMessages[Math.floor(Math.random() * logMessages.length)];
const timestamp = new Date().toLocaleTimeString();
logLines.update(lines => {
const newLines = [...lines, `[${timestamp}] ${randomLog}`];
return newLines.slice(-24);
});
}, 1200);
// Обновление системной статистики
statsInterval = setInterval(() => {
systemStats.set({
cpu: Math.floor(Math.random() * 30 + 10),
memory: Math.floor(Math.random() * 40 + 30),
disk: Math.floor(Math.random() * 20 + 10),
network: Math.floor(Math.random() * 100 + 50),
processes: Math.floor(Math.random() * 50 + 200),
uptime: "12d 4h 32m",
load: `${(Math.random() * 0.5 + 0.1).toFixed(2)}, ${(Math.random() * 0.4 + 0.08).toFixed(2)}, ${(Math.random() * 0.3 + 0.05).toFixed(2)}`
});
}, 5000);
// Генерация файловой системы
const fileSystemMessages = [
"/home/58team/projects/retro-blog/src/main.svelte [98%]",
"/var/log/system/performance.log [2.3MB]",
"/etc/config/retro-theme.conf [LOADED]",
"/usr/bin/glitch-renderer [RUNNING]",
"/tmp/cache/vhs-effects.bin [CACHED]",
"/mnt/data/blog-posts/2024/ [12 ITEMS]"
];
fileSystemInterval = setInterval(() => {
const randomFile = fileSystemMessages[Math.floor(Math.random() * fileSystemMessages.length)];
fileSystemLines.update(lines => {
const newLines = [...lines, randomFile];
return newLines.slice(-6);
});
}, 1000);
return () => {
clearInterval(sloganInterval);
clearInterval(logInterval);
clearInterval(statsInterval);
clearInterval(fileSystemInterval);
};
});
</script>
<section class="hero-section">
<Container fluid class="h-100">
<Row class="h-100 g-3">
<!-- Левый столбец - Блоки 1 и 3 под друг другом -->
<Col lg={4} class="h-100">
<Row class="h-100 g-3">
<!-- Блок 1: Логи (верхний) -->
<Col class="h-50">
<div class="terminal-wrapper h-100">
<CRTScreen class="logs-terminal h-100">
<div class="terminal-header">
<span class="blink"></span> SYSTEM LOGS
</div>
<div class="terminal-body">
<div class="logs-stream">
{#each $logLines as line}
<div class="log-line">
<span class="log-time">[{new Date().toLocaleTimeString()}]</span>
<span class="log-message">{line}</span>
</div>
{/each}
</div>
</div>
</CRTScreen>
</div>
</Col>
<!-- Блок 3: Файловая система (нижний) -->
<Col class="h-50">
<div class="terminal-wrapper h-100">
<CRTScreen class="filesystem-terminal h-100">
<div class="terminal-header">
<span class="blink"></span> FILE SYSTEM
</div>
<div class="terminal-body">
<div class="filesystem-stream">
{#each $fileSystemLines as line}
<div class="file-line">
<span class="file-icon">📁</span>
<span class="file-path">{line}</span>
<span class="file-status">[ACTIVE]</span>
</div>
{/each}
</div>
</div>
</CRTScreen>
</div>
</Col>
</Row>
</Col>
<!-- Правый столбец - Основной терминал -->
<Col lg={8} class="h-100">
<div class="terminal-wrapper h-100">
<CRTScreen class="main-terminal h-100">
<div class="terminal-header">
<span class="blink"></span> [58] TEAM - SYSTEM STATUS
</div>
<div class="terminal-body">
<!-- ASCII логотип -->
<div class="ascii-art text-center mb-3">
<pre class="ascii-logo">{asciiLogo}</pre>
</div>
<!-- Слоган -->
<div class="slogan-container mb-3">
<div class="command-line">
<span class="user">team@58server</span>
<span class="path">:~$</span>
<span class="command">echo "{@html $currentSlogan.replace(/ /g, '&nbsp;')}"</span>
</div>
<div class="output-line">
<span class="output">{$currentSlogan}</span>
</div>
</div>
<!-- Системная информация -->
<div class="system-monitor mb-3">
<div class="monitor-header d-flex justify-content-between">
<span class="title">SYSTEM RESOURCES</span>
<span class="refresh">REFRESH: 5s</span>
</div>
<div class="stats-grid">
<!-- CPU -->
<div class="stat-row d-flex align-items-center gap-2">
<span class="stat-label">CPU:</span>
<div class="progress flex-grow-1">
<div class="progress-bar bg-success" style="width: {$systemStats.cpu}%"></div>
</div>
<span class="stat-value">{$systemStats.cpu}%</span>
</div>
<!-- Memory -->
<div class="stat-row d-flex align-items-center gap-2">
<span class="stat-label">MEM:</span>
<div class="progress flex-grow-1">
<div class="progress-bar bg-info" style="width: {$systemStats.memory}%"></div>
</div>
<span class="stat-value">{$systemStats.memory}%</span>
</div>
<!-- Disk -->
<div class="stat-row d-flex align-items-center gap-2">
<span class="stat-label">DISK:</span>
<div class="progress flex-grow-1">
<div class="progress-bar bg-warning" style="width: {$systemStats.disk}%"></div>
</div>
<span class="stat-value">{$systemStats.disk}%</span>
</div>
<!-- Network -->
<div class="stat-row d-flex align-items-center gap-2">
<span class="stat-label">NET:</span>
<span class="stat-value">{$systemStats.network} Mbps</span>
</div>
<!-- Processes -->
<div class="stat-row d-flex align-items-center gap-2">
<span class="stat-label">PROC:</span>
<span class="stat-value">{$systemStats.processes}</span>
</div>
<!-- Uptime -->
<div class="stat-row d-flex align-items-center gap-2">
<span class="stat-label">UPTIME:</span>
<span class="stat-value">{$systemStats.uptime}</span>
</div>
<!-- Load -->
<div class="stat-row d-flex align-items-center gap-2">
<span class="stat-label">LOAD:</span>
<span class="stat-value">{$systemStats.load}</span>
</div>
</div>
</div>
<!-- Статус сервисов -->
<div class="services-status">
<div class="service-item d-flex justify-content-between">
<span class="service-name">Web Server</span>
<span class="service-status running">● RUNNING</span>
</div>
<div class="service-item d-flex justify-content-between">
<span class="service-name">Database</span>
<span class="service-status running">● RUNNING</span>
</div>
<div class="service-item d-flex justify-content-between">
<span class="service-name">Cache</span>
<span class="service-status running">● RUNNING</span>
</div>
</div>
</div>
</CRTScreen>
</div>
</Col>
</Row>
</Container>
</section>
<style>
.hero-section {
height: 100vh;
background: var(--bg-dark);
padding: 20px;
overflow: hidden;
}
.terminal-wrapper {
display: flex;
flex-direction: column;
}
/* Фиксированные размеры терминалов */
.logs-terminal,
.main-terminal,
.filesystem-terminal {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.terminal-header {
background: var(--primary-green);
color: var(--bg-dark);
padding: 12px;
font-weight: bold;
border-bottom: 2px solid var(--bg-dark);
font-size: 0.9rem;
font-family: 'Courier New', monospace;
flex-shrink: 0;
}
.terminal-body {
flex: 1;
padding: 15px;
overflow: auto;
background: rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
}
/* Стили для основного терминала */
.ascii-logo pre {
color: var(--primary-green);
font-family: 'Courier New', monospace;
font-size: 12px;
line-height: 1.1;
margin: 0;
}
.slogan-container {
background: rgba(0, 0, 0, 0.3);
border: 1px solid var(--terminal-border);
padding: 15px;
border-radius: 5px;
}
.command-line {
color: var(--secondary-pink);
font-family: 'Courier New', monospace;
margin-bottom: 5px;
font-size: 0.9rem;
}
.user {
color: var(--accent-blue);
font-weight: bold;
}
.path {
color: var(--primary-green);
}
.command {
color: var(--text-secondary);
}
.output-line {
color: var(--primary-green);
font-family: 'Courier New', monospace;
font-size: 0.9rem;
}
.system-monitor {
background: rgba(0, 0, 0, 0.3);
border: 1px solid var(--terminal-border);
padding: 15px;
border-radius: 5px;
}
.monitor-header {
color: var(--text-muted);
font-size: 0.9rem;
margin-bottom: 15px;
}
.stats-grid {
display: flex;
flex-direction: column;
gap: 10px;
}
.stat-row {
font-size: 0.9rem;
}
.stat-label {
color: var(--text-muted);
min-width: 50px;
font-weight: bold;
}
.stat-value {
color: var(--primary-green);
font-weight: bold;
min-width: 60px;
text-align: right;
}
/* Стили для логов */
.logs-stream {
flex: 1;
overflow: hidden;
position: relative;
}
.log-line {
color: var(--primary-green);
font-family: 'Courier New', monospace;
font-size: 0.8rem;
margin-bottom: 6px;
opacity: 0;
animation: logAppear 0.5s ease forwards, logScroll 12s linear forwards;
}
.log-time {
color: var(--text-muted);
margin-right: 8px;
font-size: 0.7rem;
}
/* Стили для файловой системы */
.filesystem-stream {
flex: 1;
overflow: hidden;
position: relative;
}
.file-line {
color: var(--primary-green);
font-family: 'Courier New', monospace;
font-size: 0.8rem;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 8px;
opacity: 0;
animation: logAppear 0.5s ease forwards, logScroll 10s linear forwards;
}
.file-icon {
font-size: 0.9rem;
}
.file-path {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-status {
color: var(--secondary-pink);
font-size: 0.7rem;
flex-shrink: 0;
}
/* Статус сервисов */
.services-status {
background: rgba(0, 0, 0, 0.3);
border: 1px solid var(--terminal-border);
padding: 10px;
border-radius: 5px;
}
.service-item {
margin-bottom: 6px;
font-size: 0.9rem;
}
.service-name {
color: var(--text-muted);
}
.service-status {
font-weight: bold;
}
.service-status.running {
color: var(--primary-green);
}
/* Анимации */
@keyframes logAppear {
to {
opacity: 0.9;
}
}
@keyframes logScroll {
from {
transform: translateY(100%);
}
to {
transform: translateY(-100%);
}
}
/* Адаптивность */
@media (max-width: 992px) {
.hero-section {
padding: 15px;
}
.terminal-body {
padding: 12px;
}
.ascii-logo pre {
font-size: 10px;
}
}
@media (max-width: 768px) {
.hero-section {
height: auto;
min-height: 100vh;
padding: 10px;
}
/* На мобильных - вертикальное расположение */
.h-50 {
height: 300px !important;
}
.terminal-wrapper {
margin-bottom: 15px;
}
.ascii-logo pre {
font-size: 8px;
}
.terminal-header {
padding: 10px;
font-size: 0.8rem;
}
.terminal-body {
padding: 10px;
}
}
@media (max-width: 576px) {
.ascii-logo pre {
font-size: 6px;
}
.slogan-container {
padding: 10px;
}
.system-monitor {
padding: 10px;
}
.stat-row {
font-size: 0.8rem;
}
.stat-label {
min-width: 40px;
}
.stat-value {
min-width: 50px;
}
}
</style>

View file

@ -0,0 +1,161 @@
<script>
import GlitchText from '$lib/components/GlitchText.svelte';
import CRTScreen from '$lib/components/CRTScreen.svelte';
export let projects = [];
</script>
<section class="projects-section">
<div class="section-title">
<GlitchText text="НАШИ ПРОЕКТЫ" size="2rem" />
</div>
<div class="projects-container">
<CRTScreen class="projects-terminal">
<div class="terminal-header">
<span class="blink"></span> ACTIVE PROJECTS
</div>
<div class="projects-grid">
{#each projects as project}
<div class="project-card">
<div class="project-header">
<h3 class="project-name">{project.name}</h3>
<span class:project-status={project.status.toLowerCase()}>
{project.status}
</span>
</div>
<p class="project-description">{project.description}</p>
<div class="project-tech">
{#each project.tech as tech}
<span class="tech-tag">{tech}</span>
{/each}
</div>
<div class="project-meta">
<span class="project-year">{project.year}</span>
</div>
</div>
{/each}
</div>
</CRTScreen>
</div>
</section>
<style>
.projects-section {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px;
background: #111;
}
.section-title {
margin-bottom: 40px;
}
.projects-container {
width: 100%;
max-width: 1200px;
height: 70vh;
}
.projects-terminal {
height: 100%;
}
.projects-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 20px;
padding: 20px;
height: calc(100% - 60px);
overflow-y: auto;
}
.project-card {
background: rgba(0, 0, 0, 0.5);
border: 1px solid #33ff00;
padding: 20px;
border-radius: 4px;
transition: all 0.3s ease;
}
.project-card:hover {
transform: translateY(-2px);
box-shadow: 0 0 20px rgba(51, 255, 0, 0.3);
}
.project-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.project-name {
color: #33ff00;
font-size: 1.2rem;
margin: 0;
}
.project-status {
padding: 4px 8px;
border-radius: 3px;
font-size: 0.8rem;
font-weight: bold;
}
.project-status.completed {
background: rgba(51, 255, 0, 0.2);
color: #33ff00;
border: 1px solid #33ff00;
}
.project-status.in-progress {
background: rgba(255, 255, 0, 0.2);
color: #ffff00;
border: 1px solid #ffff00;
}
.project-status.planning {
background: rgba(255, 0, 255, 0.2);
color: #ff00ff;
border: 1px solid #ff00ff;
}
.project-description {
color: #ccc;
line-height: 1.5;
margin-bottom: 15px;
}
.project-tech {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 15px;
}
.tech-tag {
background: rgba(255, 0, 255, 0.2);
color: #ff00ff;
padding: 3px 8px;
border-radius: 3px;
font-size: 0.8rem;
border: 1px solid #ff00ff;
}
.project-meta {
text-align: right;
}
.project-year {
color: #888;
font-size: 0.9rem;
}
</style>

View file

@ -0,0 +1,622 @@
<script>
import { onMount } from 'svelte';
import { writable } from 'svelte/store';
import { Container, Row, Col } from '@sveltestrap/sveltestrap';
import GlitchText from '$lib/components/GlitchText.svelte';
import CRTScreen from '$lib/components/CRTScreen.svelte';
const teamStructure = {
name: "[58] TEAM",
type: "directory",
children: [
{
name: "development",
type: "directory",
children: [
{
name: "frontend",
type: "directory",
children: [
{
name: "alexey.md",
type: "file",
member: {
id: "1",
name: "Алексей Петров",
role: "Lead Frontend Developer",
avatar: "",
bio: "Специалист по Svelte и веб-разработке. Любит чистый код и кофе.",
skills: ["Svelte", "JavaScript", "Vue", "React"],
articles: [
{ title: "Svelte для начинающих", date: "2024-01-15", views: 1243 },
{ title: "Оптимизация производительности", date: "2024-01-10", views: 876 },
{ title: "TypeScript лучшие практики", date: "2024-01-05", views: 954 }
]
}
},
{
name: "maria.md",
type: "file",
member: {
id: "2",
name: "Мария Сидорова",
role: "UI/UX Developer",
avatar: "",
bio: "Создает интерфейсы которые нравятся пользователям. Верит в силу дизайна.",
skills: ["Figma", "CSS", "Sass", "UI Design"],
articles: [
{ title: "Дизайн системы в 2024", date: "2024-01-14", views: 1102 },
{ title: "Анимации в вебе", date: "2024-01-09", views: 723 },
{ title: "Цветовые палитры", date: "2024-01-04", views: 645 }
]
}
}
]
},
{
name: "backend",
type: "directory",
children: [
{
name: "ivan.md",
type: "file",
member: {
id: "3",
name: "Иван Козлов",
role: "Backend Lead",
avatar: "",
bio: "Работает с базами данных и API. Любит сложные задачи.",
skills: ["Node.js", "Python", "PostgreSQL", "Docker"],
articles: [
{ title: "REST API лучшие практики", date: "2024-01-13", views: 987 },
{ title: "Базы данных для начинающих", date: "2024-01-08", views: 834 },
{ title: "Микросервисная архитектура", date: "2024-01-03", views: 1123 }
]
}
}
]
}
]
},
{
name: "design",
type: "directory",
children: [
{
name: "anna.md",
type: "file",
member: {
id: "4",
name: "Анна Волкова",
role: "Creative Director",
avatar: "",
bio: "Pixel perfectionist с любовью к ретро стилю.",
skills: ["Figma", "Photoshop", "Illustrator", "Blender"],
articles: [
{ title: "Ретро дизайн в 2024", date: "2024-01-12", views: 1456 },
{ title: "3D в веб дизайне", date: "2024-01-07", views: 678 },
{ title: "Шрифты и типографика", date: "2024-01-02", views: 789 }
]
}
}
]
}
]
};
const selectedMember = writable(null);
const expandedFolders = writable(new Set(['development', 'design']));
const isMobile = writable(false);
onMount(() => {
isMobile.set(window.innerWidth < 768);
const handleResize = () => isMobile.set(window.innerWidth < 768);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
});
function toggleFolder(folderName) {
expandedFolders.update(set => {
const newSet = new Set(set);
newSet.has(folderName) ? newSet.delete(folderName) : newSet.add(folderName);
return newSet;
});
}
function selectMember(member) {
selectedMember.set(member);
}
function isExpanded(folderName) {
return $expandedFolders.has(folderName);
}
function formatDate(dateString) {
return new Date(dateString).toLocaleDateString('ru-RU');
}
</script>
<section class="team-section">
<div class="section-title">
<GlitchText text="TEAM DIRECTORY" size="2rem" />
</div>
<Container fluid>
<Row class="g-3">
<Col lg={4} class="terminal-col">
<div class="terminal-wrapper h-100">
<CRTScreen class="directory-terminal h-100">
<div class="terminal-header">
<span class="blink"></span> TEAM STRUCTURE
</div>
<div class="terminal-body">
<div class="directory-tree">
<div class="tree-item directory root" on:click={() => toggleFolder('root')}>
<span class="folder-icon">{isExpanded('root') ? '📂' : '📁'}</span>
<span class="item-name">[58] TEAM/</span>
</div>
{#each teamStructure.children as department}
<div class="tree-branch">
<div class="tree-item directory" on:click={() => toggleFolder(department.name)}>
<span class="folder-icon">{isExpanded(department.name) ? '📂' : '📁'}</span>
<span class="item-name">{department.name}/</span>
</div>
{#if isExpanded(department.name)}
{#each department.children as team}
<div class="tree-sub-branch">
<div class="tree-item directory" on:click={() => toggleFolder(team.name)}>
<span class="folder-icon">{isExpanded(team.name) ? '📂' : '📁'}</span>
<span class="item-name">{team.name}/</span>
</div>
{#if isExpanded(team.name)}
{#each team.children as member}
<div class="tree-item file" on:click={() => selectMember(member.member)}>
<span class="file-icon">📄</span>
<span class="item-name">{member.name}</span>
</div>
{/each}
{/if}
</div>
{/each}
{/if}
</div>
{/each}
</div>
<div class="terminal-footer">
<div class="footer-stats">
<span class="stat">Members: 4</span>
<span class="stat">Articles: 12</span>
</div>
</div>
</div>
</CRTScreen>
</div>
</Col>
<Col lg={8} class="terminal-col">
<div class="terminal-wrapper h-100">
<CRTScreen class="member-terminal h-100">
<div class="terminal-header">
<span class="blink"></span>
{#if $selectedMember}
MEMBER PROFILE: {$selectedMember.name}
{:else}
SELECT TEAM MEMBER
{/if}
</div>
<div class="terminal-body">
{#if $selectedMember}
<div class="member-header">
<h3 class="member-name">{$selectedMember.name}</h3>
<p class="member-role">{$selectedMember.role}</p>
<p class="member-bio">{$selectedMember.bio}</p>
</div>
<div class="member-skills">
<h4 class="section-title">SKILLS:</h4>
<div class="skills-grid">
{#each $selectedMember.skills as skill}
<span class="skill-tag">{skill}</span>
{/each}
</div>
</div>
<div class="member-articles">
<h4 class="section-title">LATEST ARTICLES:</h4>
<div class="articles-list">
{#each $selectedMember.articles as article, index}
<div class="article-item">
<div class="article-header">
<span class="article-number">[{index + 1}]</span>
<span class="article-title">{article.title}</span>
</div>
<div class="article-meta">
<span class="article-date">{formatDate(article.date)}</span>
<span class="article-views">👁️{article.views}</span>
</div>
</div>
{/each}
</div>
</div>
<div class="member-stats">
<h4 class="section-title">STATISTICS:</h4>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-label">Articles:</span>
<span class="stat-value">{$selectedMember.articles.length}</span>
</div>
<div class="stat-item">
<span class="stat-label">Total Views:</span>
<span class="stat-value">
{$selectedMember.articles.reduce((sum, article) => sum + article.views, 0).toLocaleString()}
</span>
</div>
<div class="stat-item">
<span class="stat-label">Avg. Views:</span>
<span class="stat-value">
{Math.round($selectedMember.articles.reduce((sum, article) => sum + article.views, 0) / $selectedMember.articles.length).toLocaleString()}
</span>
</div>
</div>
</div>
{:else}
<div class="no-selection">
<div class="terminal-message">
<pre class="ascii-art">
╔══════════════════════════════════════════════╗
║ ║
║ Select a team member from the directory ║
║ to view their profile and latest articles ║
║ ║
║ Use the file tree on the left to navigate ║
║ through our team structure ║
║ ║
╚══════════════════════════════════════════════╝
</pre>
</div>
</div>
{/if}
</div>
</CRTScreen>
</div>
</Col>
</Row>
</Container>
</section>
<style>
.team-section {
min-height: 100vh;
padding: 40px 20px;
background: var(--bg-darker);
}
.section-title {
text-align: center;
margin-bottom: 30px;
}
.terminal-col {
height: 70vh;
}
.terminal-wrapper {
display: flex;
flex-direction: column;
}
.directory-terminal, .member-terminal {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.terminal-header {
background: var(--primary-green);
color: var(--bg-dark);
padding: 12px;
font-weight: bold;
border-bottom: 2px solid var(--bg-dark);
font-size: 0.9rem;
font-family: 'Courier New', monospace;
flex-shrink: 0;
}
.terminal-body {
flex: 1;
padding: 20px;
overflow: auto;
background: rgba(0, 0, 0, 0.1);
}
/* Дерево директорий */
.directory-tree {
font-family: 'Courier New', monospace;
font-size: 0.9rem;
line-height: 1.6;
}
.tree-branch {
margin-left: 20px;
}
.tree-sub-branch {
margin-left: 25px;
}
.tree-item {
cursor: pointer;
padding: 4px 8px;
border-radius: 3px;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
}
.tree-item:hover {
background: rgba(51, 255, 0, 0.1);
}
.tree-item.directory {
color: var(--primary-green);
font-weight: bold;
}
.tree-item.file {
color: var(--text-secondary);
}
.tree-item.root {
color: var(--secondary-pink);
font-weight: bold;
margin-bottom: 15px;
}
.folder-icon, .file-icon {
font-size: 1rem;
}
.terminal-footer {
margin-top: 20px;
padding-top: 15px;
border-top: 1px solid var(--terminal-border);
}
.footer-stats {
display: flex;
justify-content: space-around;
font-size: 0.8rem;
color: var(--text-muted);
}
/* Информация о сотруднике */
.member-header {
margin-bottom: 25px;
padding-bottom: 15px;
border-bottom: 1px solid var(--terminal-border);
}
.member-name {
color: var(--primary-green);
font-size: 1.8rem;
margin-bottom: 8px;
}
.member-role {
color: var(--secondary-pink);
font-size: 1.1rem;
font-weight: bold;
margin-bottom: 15px;
}
.member-bio {
color: var(--text-secondary);
line-height: 1.6;
font-size: 0.95rem;
}
.section-title {
color: var(--primary-green);
font-size: 1.1rem;
margin-bottom: 15px;
font-weight: bold;
}
.skills-grid {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 25px;
}
.skill-tag {
background: rgba(51, 255, 0, 0.2);
color: var(--primary-green);
padding: 6px 12px;
border-radius: 4px;
font-size: 0.85rem;
border: 1px solid var(--terminal-border);
font-family: 'Courier New', monospace;
}
.articles-list {
margin-bottom: 25px;
}
.article-item {
background: rgba(0, 0, 0, 0.2);
border: 1px solid var(--terminal-border);
border-radius: 4px;
padding: 12px;
margin-bottom: 10px;
transition: all 0.3s ease;
}
.article-item:hover {
background: rgba(51, 255, 0, 0.1);
transform: translateX(5px);
}
.article-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.article-number {
color: var(--secondary-pink);
font-weight: bold;
font-family: 'Courier New', monospace;
}
.article-title {
color: var(--text-secondary);
font-weight: 500;
flex: 1;
}
.article-meta {
display: flex;
justify-content: space-between;
font-size: 0.8rem;
color: var(--text-muted);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
}
.stat-item {
background: rgba(0, 0, 0, 0.2);
border: 1px solid var(--terminal-border);
border-radius: 4px;
padding: 12px;
text-align: center;
}
.stat-label {
color: var(--text-muted);
font-size: 0.8rem;
display: block;
margin-bottom: 5px;
}
.stat-value {
color: var(--primary-green);
font-weight: bold;
font-size: 1.1rem;
}
/* Сообщение при отсутствии выбора */
.no-selection {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.terminal-message {
text-align: center;
}
.ascii-art pre {
color: var(--primary-green);
font-family: 'Courier New', monospace;
font-size: 10px;
line-height: 1.2;
margin: 0;
}
/* Адаптивность */
@media (max-width: 992px) {
.terminal-col {
height: 50vh;
}
.member-name {
font-size: 1.5rem;
}
.stats-grid {
grid-template-columns: 1fr;
gap: 10px;
}
}
@media (max-width: 768px) {
.team-section {
padding: 20px 15px;
}
.terminal-col {
height: 400px;
margin-bottom: 20px;
}
.terminal-body {
padding: 15px;
}
.tree-branch {
margin-left: 15px;
}
.tree-sub-branch {
margin-left: 20px;
}
.member-name {
font-size: 1.3rem;
}
.skills-grid {
gap: 6px;
}
.skill-tag {
padding: 4px 8px;
font-size: 0.8rem;
}
.ascii-art pre {
font-size: 8px;
}
}
@media (max-width: 576px) {
.terminal-col {
height: 350px;
}
.terminal-body {
padding: 10px;
}
.member-name {
font-size: 1.2rem;
}
.article-header {
flex-direction: column;
align-items: flex-start;
gap: 5px;
}
.ascii-art pre {
font-size: 6px;
}
}
</style>

View file

@ -0,0 +1,95 @@
<script>
import GlitchText from './GlitchText.svelte';
import CRTScreen from './CRTScreen.svelte';
export let member = {
id: '',
name: '',
role: '',
avatar: '',
bio: ''
};
</script>
<CRTScreen class="team-card">
<div class="card-content">
<div class="avatar">
{#if member.avatar}
<img src={member.avatar} alt={member.name} />
{:else}
<div class="avatar-placeholder">👤</div>
{/if}
</div>
<h3 class="member-name">
<GlitchText text={member.name} size="1.1rem" />
</h3>
<p class="member-role">{member.role}</p>
<p class="member-bio">{member.bio}</p>
<div class="card-actions">
<a href={`/team/${member.id}`} class="view-profile">Профиль →</a>
</div>
</div>
</CRTScreen>
<style>
.team-card {
text-align: center;
margin-bottom: 20px;
}
.avatar {
width: 80px;
height: 80px;
border-radius: 50%;
margin: 0 auto 15px;
overflow: hidden;
border: 2px solid #33ff00;
}
.avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatar-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
background: #222;
}
.member-name {
margin: 0 0 5px 0;
color: #33ff00;
}
.member-role {
color: #ff00ff;
margin: 0 0 10px 0;
font-weight: bold;
}
.member-bio {
color: #ccc;
font-size: 0.9rem;
line-height: 1.4;
margin-bottom: 15px;
}
.view-profile {
color: #ff00ff;
text-decoration: none;
}
.view-profile:hover {
text-decoration: underline;
}
</style>

43
src/lib/config.js Normal file
View file

@ -0,0 +1,43 @@
export const siteConfig = {
title: 'DevTeam Blog',
description: 'Блог команды разработчиков о технологиях и разработке',
baseUrl: import.meta.env.VITE_BASE_URL || 'http://localhost:5173',
apiUrl: import.meta.env.VITE_API_URL || 'http://localhost:3000/api',
team: [
{
id: 1,
name: 'Иван Иванов',
position: 'Senior Frontend Developer'
},
{
id: 2,
name: 'Петр Петров',
position: 'Fullstack Developer'
},
{
id: 3,
name: 'Мария Сидорova',
position: 'UI/UX Designer'
},
{
id: 4,
name: 'Алексей Смирнов',
position: 'DevOps Engineer'
}
],
socialLinks: {
github: 'https://github.com/devteam',
twitter: 'https://twitter.com/devteam',
linkedin: 'https://linkedin.com/company/devteam'
},
features: [
'Веб-разработка',
'Мобильные приложения',
'UI/UX дизайн',
'DevOps и инфраструктура',
'Консалтинг и аудит'
]
};

1
src/lib/index.js Normal file
View file

@ -0,0 +1 @@
// Reexport your entry components here

20
src/lib/services/api.js Normal file
View file

@ -0,0 +1,20 @@
import axios from "axios";
const API_BASE_URL = 'http://localhost:3000/api';
const apiClient = axios.create({
baseURL: API_BASE_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
}
});
apiClient.interceptors.response.use(
(response) => response,
(error) => {
console.error('API Error:', error);
throw error;
}
);
export default apiClient;

View file

@ -0,0 +1,164 @@
import apiClient from './api.js';
export const blogService = {
// Получить все статьи
async getBlogPosts(params = {}) {
try {
const response = await apiClient.get('/posts', { params });
return response.data;
} catch (error) {
console.error('Error fetching blog posts:', error);
return getMockBlogPosts();
}
},
// Получить статью по ID
async getPostById(id) {
try {
const response = await apiClient.get(`/posts/${id}`);
return response.data;
} catch (error) {
console.error('Error fetching post:', error);
return getMockBlogPosts().find(post => post.id === id) || null;
}
},
// Получить статьи по категории
async getPostsByCategory(category, params = {}) {
try {
const response = await apiClient.get('/posts', {
params: { category, ...params }
});
return response.data;
} catch (error) {
console.error('Error fetching posts by category:', error);
return getMockBlogPosts().filter(post => post.category === category);
}
},
// Получить статьи по тегу
async getPostsByTag(tag, params = {}) {
try {
const response = await apiClient.get('/posts', {
params: { tag, ...params }
});
return response.data;
} catch (error) {
console.error('Error fetching posts by tag:', error);
return getMockBlogPosts().filter(post => post.tags.includes(tag));
}
},
// Получить статьи по автору
async getPostsByAuthor(username, params = {}) {
try {
const response = await apiClient.get('/posts', {
params: { username, ...params }
});
return response.data;
} catch (error) {
console.error('Error fetching posts by author:', error);
return getMockBlogPosts().filter(post => post.username === username);
}
},
// Получить метаданные для фильтров
async getBlogMetadata() {
try {
const posts = await this.getBlogPosts();
return generateMetadataFromPosts(posts);
} catch (error) {
console.error('Error fetching blog metadata:', error);
return generateMetadataFromPosts(getMockBlogPosts());
}
}
};
// Генерация метаданных из списка постов
function generateMetadataFromPosts(posts) {
const categories = [...new Set(posts.map(post => post.category))];
const tags = [...new Set(posts.flatMap(post => post.tags || []))];
const authors = [...new Set(posts.map(post => post.username))];
return {
categories: categories.map(cat => ({
name: cat,
count: posts.filter(post => post.category === cat).length
})),
tags: tags.map(tag => ({
name: tag,
count: posts.filter(post => post.tags && post.tags.includes(tag)).length
})),
authors: authors.map(author => ({
name: author,
count: posts.filter(post => post.username === author).length
})),
totalPosts: posts.length
};
}
// Mock данные соответствующие новой структуре API
function getMockBlogPosts() {
return [
{
id: "1",
title: "Svelte 5: Полное руководство",
description: "Изучите все нововведения в Svelte 5, включая runes и улучшенную реактивность",
category: "frontend",
tags: ["svelte", "javascript", "frontend"],
username: "Алексей Петров",
content: "adasdasdasdasd",
updatedAt: "2024-01-20T10:00:00Z"
},
{
id: "2",
title: "Ретро дизайн в 2024 году",
description: "Как использовать эстетику 90-х в современных веб-проектах",
category: "design",
tags: ["design", "retro", "ui/ux"],
username: "Мария Сидорова",
content: "adasdasdasdasd",
updatedAt: "2024-01-18T14:30:00Z"
},
{
id: "3",
title: "Лучшие практики безопасности API",
description: "Полное руководство по защите вашего API от уязвимостей",
category: "backend",
tags: ["security", "api", "backend"],
username: "Иван Козлов",
content: "adasdasdasdasd",
updatedAt: "2024-01-15T09:15:00Z"
},
{
id: "4",
title: "Оптимизация производительности веб-приложений",
description: "Современные техники для ускорения загрузки и отзывчивости",
category: "frontend",
tags: ["performance", "optimization", "javascript"],
username: "Алексей Петров",
content: "adasdasdasdasd",
updatedAt: "2024-01-12T16:45:00Z"
},
{
id: "5",
title: "Docker для веб-разработчиков",
description: "Практическое руководство по контейнеризации приложений",
category: "devops",
tags: ["docker", "devops", "containers"],
content: "adasdasdasdasd",
username: "Иван Козлов",
updatedAt: "2024-01-10T11:20:00Z"
},
{
id: "6",
title: "CSS Grid vs Flexbox: Когда что использовать",
description: "Подробное сравнение двух основных CSS технологий верстки",
category: "frontend",
tags: ["css", "grid", "flexbox", "design"],
username: "Мария Сидорова",
content: "adasdasdasdasd",
updatedAt: "2024-01-08T13:10:00Z"
}
];
}

View file

@ -0,0 +1,110 @@
import apiClient from './api.js';
export const teamService = {
// Получить всех членов команды
async getTeamMembers() {
try {
const response = await apiClient.get('/team');
return response.data;
} catch (error) {
console.error('Error fetching team members:', error);
// Возвращаем mock данные в случае ошибки
return getMockTeamMembers();
}
},
// Получить конкретного члена команды
async getTeamMember(id) {
try {
const response = await apiClient.get(`/team/${id}`);
return response.data;
} catch (error) {
console.error('Error fetching team member:', error);
return null;
}
},
// Получить случайных членов команды для карусели
async getRandomMembers(limit = 4) {
try {
const response = await apiClient.get('/team/random', {
params: { limit }
});
return response.data;
} catch (error) {
console.error('Error fetching random members:', error);
return getMockTeamMembers().slice(0, limit);
}
}
};
// Mock данные для разработки
function getMockTeamMembers() {
return [
{
id: 1,
username: "coma",
name: "Coma",
role: "DevOps Engineer",
specialty: "Системное администрирование & Девопс",
description: "Отвечает за инфраструктуру, развертывание и мониторинг. Следит за тем, чтобы все системы работали как часы.",
skills: ["Docker", "Kubernetes", "AWS", "CI/CD", "Linux", "Monitoring"],
avatar: "⚙️",
joinDate: "2023",
projects: ["Infrastructure", "Deployment", "Monitoring"],
motto: "Если что-то можно автоматизировать - это должно быть автоматизировано"
},
{
id: 2,
username: "muts",
name: "Muts",
role: "Embedded/Backend Developer",
specialty: "Встроенные системы & Бэкенд разработка",
description: "Работает на стыке hardware и software. Создает эффективные решения для встроенных систем и серверной части.",
skills: ["C/C++", "Python", "Embedded", "Rust", "API Design", "Microcontrollers"],
avatar: "🔌",
joinDate: "2023",
projects: ["Embedded Systems", "Backend API", "Hardware Integration"],
motto: "Код должен быть быстрым как железо, и надежным как скала"
},
{
id: 3,
username: "denzz",
name: "Denzz",
role: "Frontend Developer",
specialty: "Пользовательские интерфейсы & Веб-разработка",
description: "Создает интуитивные и красивые интерфейсы. Превращает сложные идеи в простые и элегантные решения.",
skills: ["Svelte", "JavaScript", "TypeScript", "CSS", "UI/UX", "Animation"],
avatar: "🎨",
joinDate: "2023",
projects: ["Web Interfaces", "User Experience", "Visual Design"],
motto: "Хороший интерфейс - это когда пользователь не замечает интерфейс"
},
{
id: 4,
username: "itc1205",
name: "ITC1205",
role: "CEO & Backend Developer",
specialty: "Техническое руководство & Архитектура систем",
description: "Совмещает техническое видение с бизнес-стратегией. Отвечает за архитектурные решения и развитие команды.",
skills: ["System Architecture", "Project Management", "Node.js", "Python", "Leadership", "Strategy"],
avatar: "👑",
joinDate: "2023",
projects: ["Technical Strategy", "System Architecture", "Team Leadership"],
motto: "Технологии должны решать реальные проблемы, а не создавать новые"
},
{
id: 5,
username: "d1nnoise",
name: "D1nnoise",
role: "QA Engineer",
specialty: "Тестирование & Обеспечение качества",
description: "Гарантирует качество и надежность продуктов. Находит баги до того, как их найдут пользователи.",
skills: ["Testing", "Automation", "Quality Assurance", "Bug Tracking", "Test Planning", "CI/CD"],
avatar: "🐛",
joinDate: "2023",
projects: ["Quality Assurance", "Testing Automation", "Process Improvement"],
motto: "Идеального кода не существует, но к нему нужно стремиться"
}
];
}

32
src/lib/utils/date.js Normal file
View file

@ -0,0 +1,32 @@
export function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString('ru-RU', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
}
export function formatDateTime(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString('ru-RU', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
export function getRelativeTime(dateString) {
const date = new Date(dateString);
const now = new Date();
const diffInSeconds = Math.floor((now - date) / 1000);
if (diffInSeconds < 60) return 'только что';
if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)} мин. назад`;
if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)} ч. назад`;
if (diffInSeconds < 604800) return `${Math.floor(diffInSeconds / 86400)} дн. назад`;
return formatDate(dateString);
}

26
src/lib/utils/markdown.js Normal file
View file

@ -0,0 +1,26 @@
import { marked } from 'marked';
import sanitizeHtml from 'sanitize-html';
marked.setOptions({
breaks: true,
gfm: true
});
const sanitizeOptions = {
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img', 'h1', 'h2', 'h3']),
allowedAttributes: {
...sanitizeHtml.defaults.allowedAttributes,
img: ['src', 'alt', 'title'],
a: ['href', 'name', 'target']
}
};
export function parseMarkdown(content) {
const html = marked.parse(content);
return sanitizeHtml(html, sanitizeOptions);
}
export function extractExcerpt(content, length = 150) {
const plainText = content.replace(/[#*`~]/g, '').replace(/\n/g, ' ');
return plainText.slice(0, length) + (plainText.length > length ? '...' : '');
}