aaaaa
This commit is contained in:
commit
5ab2d8abfd
45 changed files with 9738 additions and 0 deletions
72
src/lib/components/BlogCard.svelte
Normal file
72
src/lib/components/BlogCard.svelte
Normal 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>
|
||||
76
src/lib/components/CRTScreen.svelte
Normal file
76
src/lib/components/CRTScreen.svelte
Normal 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>
|
||||
7
src/lib/components/Error.svelte
Normal file
7
src/lib/components/Error.svelte
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<script>
|
||||
import { Alert } from '@sveltestrap/sveltestrap';
|
||||
</script>
|
||||
|
||||
<Alert color="danger" class="my-3">
|
||||
{message}
|
||||
</Alert>
|
||||
104
src/lib/components/Footer.svelte
Normal file
104
src/lib/components/Footer.svelte
Normal 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>
|
||||
58
src/lib/components/GlitchText.svelte
Normal file
58
src/lib/components/GlitchText.svelte
Normal 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>
|
||||
7
src/lib/components/Loading.svelte
Normal file
7
src/lib/components/Loading.svelte
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<script>
|
||||
import { Spinner } from '@sveltestrap/sveltestrap';
|
||||
</script>
|
||||
|
||||
<div class="text-center my-5">
|
||||
<Spinner color="primary" />
|
||||
</div>
|
||||
9
src/lib/components/MarkdownRender.svelte
Normal file
9
src/lib/components/MarkdownRender.svelte
Normal 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>
|
||||
257
src/lib/components/Navigation.svelte
Normal file
257
src/lib/components/Navigation.svelte
Normal 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>
|
||||
85
src/lib/components/RetroButton.svelte
Normal file
85
src/lib/components/RetroButton.svelte
Normal 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>
|
||||
84
src/lib/components/Sections/BlogSection.svelte
Normal file
84
src/lib/components/Sections/BlogSection.svelte
Normal 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>
|
||||
557
src/lib/components/Sections/HeroSection.svelte
Normal file
557
src/lib/components/Sections/HeroSection.svelte
Normal 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, ' ')}"</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>
|
||||
161
src/lib/components/Sections/ProjectsSection.svelte
Normal file
161
src/lib/components/Sections/ProjectsSection.svelte
Normal 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>
|
||||
622
src/lib/components/Sections/TeamSection.svelte
Normal file
622
src/lib/components/Sections/TeamSection.svelte
Normal 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>
|
||||
95
src/lib/components/TeamCard.svelte
Normal file
95
src/lib/components/TeamCard.svelte
Normal 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
43
src/lib/config.js
Normal 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
1
src/lib/index.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
// Reexport your entry components here
|
||||
20
src/lib/services/api.js
Normal file
20
src/lib/services/api.js
Normal 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;
|
||||
164
src/lib/services/blogService.js
Normal file
164
src/lib/services/blogService.js
Normal 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"
|
||||
}
|
||||
];
|
||||
}
|
||||
110
src/lib/services/teamService.js
Normal file
110
src/lib/services/teamService.js
Normal 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
32
src/lib/utils/date.js
Normal 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
26
src/lib/utils/markdown.js
Normal 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 ? '...' : '');
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue