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

24
.gitignore vendored Normal file
View file

@ -0,0 +1,24 @@
node_modules
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
/dist
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

1
.npmrc Normal file
View file

@ -0,0 +1 @@
engine-strict=true

58
README.md Normal file
View file

@ -0,0 +1,58 @@
# Svelte library
Everything you need to build a Svelte library, powered by [`sv`](https://npmjs.com/package/sv).
Read more about creating a library [in the docs](https://svelte.dev/docs/kit/packaging).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```sh
# create a new project in the current directory
npx sv create
# create a new project in my-app
npx sv create my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```sh
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
Everything inside `src/lib` is part of your library, everything inside `src/routes` can be used as a showcase or preview app.
## Building
To build your library:
```sh
npm pack
```
To create a production version of your showcase app:
```sh
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
## Publishing
Go into the `package.json` and give your package the desired name through the `"name"` option. Also consider adding a `"license"` field and point it to a `LICENSE` file which you can create from a template (one popular option is the [MIT license](https://opensource.org/license/mit/)).
To publish your library to [npm](https://www.npmjs.com):
```sh
npm publish
```

6
builder.config.json Normal file
View file

@ -0,0 +1,6 @@
{
"command": "npm run dev",
"serverUrl": "http://localhost:5173/blog",
"authenticateProxy": false,
"commitMode": "commits"
}

12
jsconfig.json Normal file
View file

@ -0,0 +1,12 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"module": "NodeNext",
"moduleResolution": "NodeNext"
}
}

2235
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

51
package.json Normal file
View file

@ -0,0 +1,51 @@
{
"name": "blog58teamfront",
"version": "0.0.1",
"scripts": {
"dev": "vite dev",
"build": "vite build && npm run prepack",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"prepack": "svelte-kit sync && svelte-package && publint"
},
"files": [
"dist",
"!dist/**/*.test.*",
"!dist/**/*.spec.*"
],
"sideEffects": [
"**/*.css"
],
"svelte": "./dist/index.js",
"types": "./dist/index.d.ts",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"svelte": "./dist/index.js"
}
},
"peerDependencies": {
"svelte": "^5.0.0"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^6.0.0",
"@sveltejs/kit": "^2.22.0",
"@sveltejs/package": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^6.0.0",
"mdsvex": "^0.12.3",
"publint": "^0.3.2",
"svelte": "^5.39.3",
"typescript": "^5.3.2",
"vite": "^7.0.4"
},
"keywords": [
"svelte"
],
"dependencies": {
"@sveltestrap/sveltestrap": "^7.1.0",
"axios": "^1.12.2",
"marked": "^16.3.0",
"sanitize-html": "^2.17.0"
}
}

215
src/app.css Normal file
View file

@ -0,0 +1,215 @@
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--primary-green: #00ff66;
--secondary-pink: #ff00ff;
--terminal-bg: #0a0a0a;
--terminal-border: #00ff66;
--text-primary: #00ff66;
--text-secondary: #cccccc;
--text-muted: #888888;
--bg-dark: #000000;
--bg-darker: #0a0a0a;
--accent-blue: #00aaff;
--accent-yellow: #ffff00;
}
body {
font-family: 'Courier New', 'Monaco', 'Consolas', monospace;
background: var(--bg-dark);
color: var(--text-primary);
line-height: 1.6;
overflow-x: hidden;
font-size: 16px;
}
/*эффект ЭЛТ монитора */
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background:
linear-gradient(
to bottom,
rgba(18, 16, 16, 0.1) 50%,
rgba(0, 0, 0, 0.1) 50%
),
radial-gradient(
circle at center,
rgba(0, 255, 102, 0.05) 0%,
transparent 70%
);
background-size: 100% 2px, 100% 100%;
z-index: 9999;
pointer-events: none;
opacity: 0.15;
}
/* Плавные анимации */
* {
transition: all 0.3s ease;
}
/*скроллбар */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-darker);
border: 1px solid var(--terminal-border);
}
::-webkit-scrollbar-thumb {
background: var(--primary-green);
border: 1px solid var(--bg-darker);
border-radius: 2px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--secondary-pink);
}
/* Выделение текста */
::selection {
background: var(--primary-green);
color: var(--bg-dark);
}
::-moz-selection {
background: var(--primary-green);
color: var(--bg-dark);
}
/* Анимации */
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
@keyframes glow {
0%, 100% { text-shadow: 0 0 5px var(--primary-green); }
50% { text-shadow: 0 0 15px var(--primary-green), 0 0 20px var(--secondary-pink); }
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-5px); }
}
.blink {
animation: blink 1s infinite;
}
.glow {
animation: glow 2s infinite;
}
.float {
animation: float 3s ease-in-out infinite;
}
/* Адаптивность */
@media (max-width: 768px) {
body {
font-size: 14px;
}
}
@media (max-width: 480px) {
body {
font-size: 12px;
}
}
button:focus,
a:focus,
input:focus,
textarea:focus {
outline: 2px solid var(--secondary-pink);
outline-offset: 2px;
}
html {
scroll-behavior: smooth;
}
p, li {
line-height: 1.8;
color: var(--text-secondary);
}
h1, h2, h3, h4, h5, h6 {
line-height: 1.3;
margin-bottom: 1rem;
}
.vhs-distortion {
position: relative;
overflow: hidden;
}
.vhs-distortion::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(
to bottom,
transparent 0%,
rgba(0, 255, 102, 0.03) 50%,
transparent 100%
);
pointer-events: none;
mix-blend-mode: overlay;
}
.h-50 {
height: 50% !important;
}
.g-3 > .row {
margin-right: calc(-0.5 * var(--bs-gutter-x));
margin-left: calc(-0.5 * var(--bs-gutter-x));
}
.g-3 > .row > * {
padding-right: calc(var(--bs-gutter-x) * 0.5);
padding-left: calc(var(--bs-gutter-x) * 0.5);
}
.logs-terminal .terminal-body,
.filesystem-terminal .terminal-body {
min-height: 50%;
}
.logs-stream,
.filesystem-stream {
scrollbar-width: thin;
}
/* Анимации для плавного появления */
@keyframes terminalAppear {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.terminal-wrapper {
animation: terminalAppear 0.5s ease-out;
}

19
src/app.html Normal file
View file

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>[58]Team Blog</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
/>
%sveltekit.head%
</head>
<body data-bs-theme="dark">
<div style="display: flex; flex-direction: column; min-height: 100vh;">%sveltekit.body%</div>
</body>
</html>

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 ? '...' : '');
}

View file

@ -0,0 +1,960 @@
<script>
import { onMount } from 'svelte';
import { writable } from 'svelte/store';
import { Row, Col } from '@sveltestrap/sveltestrap';
import { teamService } from '$lib/services/teamService.js';
import { blogService } from '$lib/services/blogService.js';
const teamMembers = writable([]);
const blogPosts = writable([]);
const loading = writable(true);
const error = writable(null);
const slogans = [
"Innovation Through Retro",
"Code Like It's 1999",
"Future Ready, Retro Style",
"Where Vintage Meets Modern",
"Digital Revolution, Classic Soul"
];
const currentSlogan = writable(slogans[0]);
const logLines = writable([]);
const currentMemberIndex = writable(0);
const isMobile = writable(false);
let sloganInterval;
let logInterval;
let memberInterval;
onMount(async () => {
isMobile.set(window.innerWidth < 768);
await loadData();
startAnimations();
const handleResize = () => isMobile.set(window.innerWidth < 768);
window.addEventListener('resize', handleResize);
return () => {
clearInterval(sloganInterval);
clearInterval(logInterval);
clearInterval(memberInterval);
window.removeEventListener('resize', handleResize);
};
});
async function loadData() {
try {
loading.set(true);
error.set(null);
const [membersData, postsData] = await Promise.all([
teamService.getRandomMembers(5),
blogService.getBlogPosts(3)
]);
teamMembers.set(membersData);
blogPosts.set(postsData);
} catch (err) {
console.error('Error loading data:', err);
error.set('Failed to load data. Please try again later.');
} finally {
loading.set(false);
}
}
function startAnimations() {
// Смена слоганов
let sloganIndex = 0;
sloganInterval = setInterval(() => {
sloganIndex = (sloganIndex + 1) % slogans.length;
currentSlogan.set(slogans[sloganIndex]);
}, 3000);
// Генерация логов
const logMessages = [
"SYSTEM: Initializing retro framework...",
"NETWORK: Connection established to main server",
"SECURITY: Firewall rules applied successfully",
"STORAGE: SSD cache optimized for performance",
"MEMORY: Allocated 2GB for glitch effects buffer",
"GPU: VHS rendering pipeline activated",
"AUDIO: Retro sound system initialized",
"UI: Terminal interface rendered successfully"
];
logInterval = setInterval(() => {
const randomLog = logMessages[Math.floor(Math.random() * logMessages.length)];
logLines.update(lines => {
const newLines = [...lines, randomLog];
return newLines.slice(-20);
});
}, 500);
// Карусель сотрудников
memberInterval = setInterval(() => {
currentMemberIndex.update(idx => (idx + 1) % $teamMembers.length);
}, 4000);
}
function nextMember() {
currentMemberIndex.update(idx => (idx + 1) % $teamMembers.length);
}
function prevMember() {
currentMemberIndex.update(idx => (idx - 1 + $teamMembers.length) % $teamMembers.length);
}
function getRandomLogMessage() {
const logTypes = [
{
prefix: "SYSTEM",
messages: [
"Initializing retro framework... [OK]",
"Memory allocation: 2GB for glitch buffer",
"VHS rendering pipeline: ACTIVE",
"Terminal interface: RENDERED",
"Audio system: RETRO MODE ENABLED"
]
},
{
prefix: "NETWORK",
messages: [
"Connected to main server: 192.168.1.58",
"Bandwidth: 1.2 Gbps UPLOAD",
"Latency: 12ms PING",
"SSL Certificate: VALID",
"VPN Tunnel: ESTABLISHED"
]
},
{
prefix: "API",
messages: [
"Team data loaded successfully",
"Blog posts fetched from database",
"API response time: 120ms",
"Cache hit ratio: 92%",
"Data synchronization: COMPLETE"
]
}
];
const type = logTypes[Math.floor(Math.random() * logTypes.length)];
const message = type.messages[Math.floor(Math.random() * type.messages.length)];
return `${type.prefix}: ${message}`;
}
</script>
<main class="tiling-layout">
{#if $error}
<div class="error-banner">
<div class="error-content">
<span class="error-icon"></span>
<span class="error-message">{$error}</span>
<button class="retry-btn" on:click={loadData}>Retry</button>
</div>
</div>
{/if}
<Row class="h-100 g-0">
<!-- Блок 1: Левая панель (50vw × 100vh) -->
<Col lg={6} class="h-100">
<div class="panel left-panel">
<div class="panel-content">
<!-- Логотип и слоган -->
<div class="welcome-section">
<div class="main-logo">
<span class="logo-bracket">[</span>
<span class="logo-number">58</span>
<span class="logo-bracket">]</span>
<span class="logo-text">Team</span>
</div>
<div class="slogan-display">
<div class="slogan-line">
<span class="prompt">$</span>
<span class="slogan">{$currentSlogan}</span>
</div>
<div class="cursor blink">_</div>
</div>
{#if $loading}
<div class="loading-indicator">
<div class="loading-spinner"></div>
<span>Loading data from API...</span>
</div>
{/if}
</div>
</div>
<!-- Бегущие логи на фоне -->
<div class="logs-background">
{#each Array(30) as _, i}
<div
class="log-line"
style="
animation-delay: {i * 2}s;
left: {Math.random() * 90 + 5}%;
animation-duration: {Math.random() * 10 + 20}s;
opacity: {Math.random() * 0.3 + 0.1};
font-size: {Math.random() * 4 + 10}px;
"
>
{getRandomLogMessage()}
</div>
{/each}
</div>
</div>
</Col>
<!-- Правая колонка (50vw × 100vh) -->
<Col lg={6} class="h-100">
<Row class="h-100 g-0">
<!-- Блок 2: Верхняя правая панель (50vw × 50vh) -->
<Col lg={12} class="h-50">
<div class="panel team-panel">
<div class="panel-header">
<h3>MEET THE TEAM</h3>
<a href="/team" class="view-all">VIEW ALL →</a>
</div>
<div class="team-carousel">
{#if $loading}
<div class="loading-state">
<div class="loading-spinner"></div>
<span>Loading team members...</span>
</div>
{:else if $teamMembers.length === 0}
<div class="empty-state">
<span class="empty-icon">👥</span>
<span>No team members found</span>
</div>
{:else}
{#each [$teamMembers[$currentMemberIndex]] as member}
<div class="team-member-card">
<div class="member-avatar">{member.avatar}</div>
<div class="member-info">
<h4 class="member-name">{member.name}</h4>
<p class="member-role">{member.role}</p>
<p class="member-bio">{member.bio}</p>
<div class="member-skills">
{#each member.skills.slice(0, 3) as skill}
<span class="skill-tag">{skill}</span>
{/each}
</div>
</div>
</div>
{/each}
<div class="carousel-controls">
<button class="control-btn" on:click={prevMember}></button>
<div class="carousel-dots">
{#each $teamMembers as _, index}
<span
class:active={index === $currentMemberIndex}
class="dot"
on:click={() => currentMemberIndex.set(index)}
></span>
{/each}
</div>
<button class="control-btn" on:click={nextMember}></button>
</div>
{/if}
</div>
</div>
</Col>
<!-- Блок 3: Нижняя правая панель (50vw × 50vh) -->
<Col lg={12} class="h-50">
<div class="panel blog-panel">
<div class="panel-header">
<h3>LATEST ARTICLES</h3>
<a href="/blog" class="view-all">VIEW ALL →</a>
</div>
<div class="articles-list">
{#if $loading}
<div class="loading-state">
<div class="loading-spinner"></div>
<span>Loading articles...</span>
</div>
{:else if $blogPosts.length === 0}
<div class="empty-state">
<span class="empty-icon">📝</span>
<span>No articles found</span>
</div>
{:else}
{#each $blogPosts as post}
<article class="article-preview">
<div class="article-meta">
<span class="article-date">{new Date(post.date).toLocaleDateString()}</span>
<span class="article-author">by {post.author}</span>
</div>
<h4 class="article-title">{post.title}</h4>
<p class="article-excerpt">{post.excerpt}</p>
<div class="article-footer">
<div class="article-tags">
{#each post.tags.slice(0, 2) as tag}
<span class="tag">#{tag}</span>
{/each}
</div>
<a href={`/blog/${post.slug}`} class="read-more">READ →</a>
</div>
</article>
{/each}
{/if}
</div>
</div>
</Col>
</Row>
</Col>
</Row>
</main>
<style>
.tiling-layout {
height: 100vh;
padding-top: 50px;
background: var(--bg-dark);
overflow: hidden;
}
.panel {
height: 100%;
border: 1px solid var(--terminal-border);
position: relative;
overflow: hidden;
}
.panel-content {
padding: 40px;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
position: relative;
z-index: 2;
}
/* Левая панель */
.left-panel {
border-right: 2px solid var(--primary-green);
}
.main-logo {
font-size: 4rem;
font-weight: bold;
margin-bottom: 30px;
font-family: 'JetBrains Mono', monospace;
}
.logo-bracket {
color: var(--secondary-pink);
}
.logo-number {
color: var(--primary-green);
}
.logo-text {
color: var(--text-secondary);
}
.slogan-display {
display: flex;
align-items: center;
gap: 10px;
font-size: 1.5rem;
font-family: 'Courier New', monospace;
}
.prompt {
color: var(--secondary-pink);
font-weight: bold;
}
.slogan {
color: var(--primary-green);
}
.cursor {
color: var(--primary-green);
animation: blink 1s infinite;
}
/* Логи на фоне */
.logs-background {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
opacity: 0.1;
pointer-events: none;
z-index: 1;
overflow: hidden;
}
.log-line {
color: var(--primary-green);
font-family: 'Courier New', monospace;
font-size: 0.8rem;
margin-bottom: 5px;
opacity: 0;
animation: logScroll 15s linear forwards;
white-space: nowrap;
}
@keyframes logScroll {
0% {
transform: translateY(100vh);
opacity: 0;
}
5% {
opacity: 0.3;
}
95% {
opacity: 0.3;
}
100% {
transform: translateY(-100px);
opacity: 0;
}
}
/* Правая верхняя панель (команда) */
.team-panel {
border-bottom: 1px solid var(--terminal-border);
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 30px 0;
margin-bottom: 20px;
}
.panel-header h3 {
color: var(--primary-green);
margin: 0;
font-size: 1.2rem;
}
.view-all {
color: var(--secondary-pink);
text-decoration: none;
font-size: 0.9rem;
transition: all 0.3s ease;
}
.view-all:hover {
color: var(--primary-green);
}
.team-carousel {
padding: 0 30px;
height: calc(100% - 80px);
display: flex;
flex-direction: column;
justify-content: center;
}
.team-member-card {
display: flex;
align-items: center;
gap: 20px;
background: rgba(0, 0, 0, 0.3);
border: 1px solid var(--terminal-border);
border-radius: 10px;
padding: 20px;
transition: all 0.3s ease;
}
.team-member-card:hover {
transform: translateX(5px);
border-color: var(--primary-green);
}
.member-avatar {
font-size: 3rem;
}
.member-info {
flex: 1;
}
.member-name {
color: var(--primary-green);
font-size: 1.3rem;
margin: 0 0 5px 0;
}
.member-role {
color: var(--secondary-pink);
font-weight: bold;
margin: 0 0 10px 0;
}
.member-bio {
color: var(--text-secondary);
margin: 0;
font-size: 0.9rem;
}
.carousel-controls {
display: flex;
align-items: center;
justify-content: center;
gap: 15px;
margin-top: 20px;
}
.control-btn {
background: rgba(51, 255, 0, 0.1);
border: 1px solid var(--primary-green);
color: var(--primary-green);
width: 30px;
height: 30px;
border-radius: 50%;
cursor: pointer;
transition: all 0.3s ease;
font-size: 1rem;
}
.control-btn:hover {
background: var(--primary-green);
color: var(--bg-dark);
}
.carousel-dots {
display: flex;
gap: 8px;
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--text-muted);
cursor: pointer;
transition: all 0.3s ease;
}
.dot.active {
background: var(--primary-green);
transform: scale(1.2);
}
/* Правая нижняя панель (статьи) */
.blog-panel {
border-top: 1px solid var(--terminal-border);
}
.articles-list {
padding: 0 30px;
height: calc(100% - 80px);
overflow-y: auto;
}
.article-preview {
background: rgba(0, 0, 0, 0.3);
border: 1px solid var(--terminal-border);
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
transition: all 0.3s ease;
}
.article-preview:hover {
border-color: var(--primary-green);
transform: translateY(-2px);
}
.article-meta {
display: flex;
justify-content: space-between;
font-size: 0.8rem;
color: var(--text-muted);
margin-bottom: 8px;
}
.article-title {
color: var(--primary-green);
font-size: 1.1rem;
margin: 0 0 8px 0;
}
.article-excerpt {
color: var(--text-secondary);
font-size: 0.9rem;
margin: 0 0 10px 0;
line-height: 1.4;
}
.read-more {
color: var(--secondary-pink);
text-decoration: none;
font-size: 0.9rem;
font-weight: bold;
transition: all 0.3s ease;
}
.read-more:hover {
color: var(--primary-green);
}
/* Адаптивность */
@media (max-width: 992px) {
.main-logo {
font-size: 3rem;
}
.slogan-display {
font-size: 1.2rem;
}
.panel-content {
padding: 30px;
}
}
@media (max-width: 768px) {
.tiling-layout {
height: auto;
min-height: 100vh;
padding-top: 40px;
}
.main-logo {
font-size: 2.5rem;
text-align: center;
}
.slogan-display {
font-size: 1rem;
justify-content: center;
}
.panel-content {
padding: 20px;
}
.team-member-card {
flex-direction: column;
text-align: center;
}
.member-avatar {
font-size: 2.5rem;
}
.panel-header {
padding: 15px 20px 0;
}
.team-carousel,
.articles-list {
padding: 0 20px;
}
}
@media (max-width: 576px) {
.main-logo {
font-size: 2rem;
}
.panel-header {
flex-direction: column;
gap: 10px;
text-align: center;
}
.team-carousel,
.articles-list {
padding: 0 15px;
}
.article-preview {
padding: 12px;
}
}
.logs-background {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
z-index: 1;
overflow: hidden;
background:
linear-gradient(180deg,
rgba(0, 255, 102, 0.03) 0%,
transparent 50%,
rgba(0, 255, 102, 0.03) 100%
);
}
.log-line {
position: absolute;
color: var(--primary-green);
font-family: 'JetBrains Mono', 'Fira Code', 'Courier New', monospace;
font-weight: 500;
white-space: nowrap;
text-shadow:
0 0 10px rgba(0, 255, 102, 0.8),
0 0 20px rgba(0, 255, 102, 0.4),
0 0 30px rgba(0, 255, 102, 0.2);
animation: logFloat infinite linear;
opacity: 0;
}
@keyframes logFloat {
0% {
transform: translateY(100vh) rotate(0deg);
opacity: 0;
}
5% {
opacity: 1;
}
95% {
opacity: 1;
}
100% {
transform: translateY(-100px) rotate(0deg);
opacity: 0;
}
}
/* эффект сканирования ЭЛТ */
.left-panel::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
to bottom,
transparent 0%,
rgba(0, 255, 102, 0.02) 10%,
transparent 20%,
rgba(0, 255, 102, 0.03) 30%,
transparent 40%,
rgba(0, 255, 102, 0.01) 50%,
transparent 60%,
rgba(0, 255, 102, 0.02) 70%,
transparent 80%,
rgba(0, 255, 102, 0.01) 90%,
transparent 100%
);
background-size: 100% 200px;
animation: scan 8s linear infinite;
pointer-events: none;
z-index: 2;
}
@keyframes scan {
0% {
background-position: 0 0;
}
100% {
background-position: 0 200px;
}
}
/* Эффект мерцания как в старых мониторах */
.left-panel::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background:
radial-gradient(
circle at 20% 80%,
rgba(0, 255, 102, 0.05) 0%,
transparent 50%
),
radial-gradient(
circle at 80% 20%,
rgba(255, 0, 255, 0.03) 0%,
transparent 50%
);
pointer-events: none;
z-index: 3;
animation: flicker 0.2s infinite;
}
@keyframes flicker {
0%, 100% { opacity: 0.97; }
50% { opacity: 0.99; }
}
/* Делаем логотип и слоган более контрастными */
.main-logo {
text-shadow:
0 0 20px rgba(0, 255, 102, 0.5),
0 0 40px rgba(0, 255, 102, 0.3),
0 0 60px rgba(0, 255, 102, 0.1);
}
.slogan {
text-shadow:
0 0 10px rgba(0, 255, 102, 0.6),
0 0 20px rgba(0, 255, 102, 0.4);
}
@media (max-width: 768px) {
.log-line {
font-size: 10px !important;
}
.panel-content {
margin: 10px;
padding: 20px;
}
.logs-background {
opacity: 0.8;
}
}
@media (max-width: 480px) {
.log-line {
font-size: 8px !important;
}
.panel-content {
margin: 5px;
padding: 15px;
}
}
.tiling-layout {
height: 100vh;
padding-top: 50px;
background: var(--bg-dark);
overflow: hidden;
}
.error-banner {
position: fixed;
top: 60px;
left: 50%;
transform: translateX(-50%);
background: rgba(255, 0, 0, 0.1);
border: 1px solid #ff4444;
border-radius: 5px;
padding: 10px 20px;
z-index: 1000;
backdrop-filter: blur(10px);
}
.error-content {
display: flex;
align-items: center;
gap: 10px;
color: #ff4444;
}
.retry-btn {
background: rgba(255, 0, 0, 0.2);
border: 1px solid #ff4444;
color: #ff4444;
padding: 5px 10px;
border-radius: 3px;
cursor: pointer;
font-size: 0.8rem;
}
.retry-btn:hover {
background: #ff4444;
color: white;
}
.loading-indicator {
display: flex;
align-items: center;
gap: 10px;
margin-top: 20px;
color: var(--primary-green);
}
.loading-state, .empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--text-muted);
gap: 10px;
}
.loading-spinner {
width: 20px;
height: 20px;
border: 2px solid var(--primary-green);
border-top: 2px solid transparent;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.empty-icon {
font-size: 2rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.member-skills {
display: flex;
gap: 5px;
margin-top: 10px;
}
.skill-tag {
background: rgba(51, 255, 0, 0.1);
color: var(--primary-green);
padding: 2px 6px;
border-radius: 3px;
font-size: 0.7rem;
border: 1px solid var(--primary-green);
}
.article-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
}
.article-tags {
display: flex;
gap: 5px;
}
.tag {
background: rgba(255, 0, 170, 0.1);
color: var(--secondary-pink);
padding: 2px 6px;
border-radius: 3px;
font-size: 0.7rem;
border: 1px solid var(--secondary-pink);
}
</style>

View file

@ -0,0 +1,942 @@
<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 activeSection = writable('mission');
const isMobile = writable(false);
const teamData = {
mission: {
title: "Наша Миссия",
icon: "",
content: {
main: "Мы создаем инновационные технологические решения, которые делают мир лучше, проще и эффективнее.",
points: [
"Разрабатывать ПО, которое решает реальные проблемы",
"Создавать продукты с душой и вниманием к деталям",
"Продвигать open-source и сообщество разработчиков",
"Обучать и вдохновлять следующее поколение инженеров"
]
}
},
values: {
title: "Наши Ценности",
icon: "",
content: {
main: "Наши ценности - это фундамент, на котором строится вся наша работа.",
points: [
{
icon: "",
title: "Инновации",
description: "Мы постоянно исследуем новые технологии и подходы"
},
{
icon: "",
title: "Качество",
description: "Каждая строка кода должна быть продумана и оптимизирована"
},
{
icon: "",
title: "Сотрудничество",
description: "Вместе мы достигаем большего, чем по отдельности"
},
{
icon: "",
title: "Обучение",
description: "Мы постоянно растем и развиваемся как профессионалы"
},
{
icon: "",
title: "Креативность",
description: "Находим нестандартные решения для сложных задач"
},
{
icon: "",
title: "Эффективность",
description: "Делаем больше за меньшее время без потери качества"
}
]
}
},
history: {
title: "Наша История",
icon: "",
content: {
main: "Путь от идеи к реализации - это история постоянного роста и развития.",
timeline: [
{
year: "2023",
event: "Основание команды",
description: "Группа энтузиастов объединилась для работы над первыми проектами"
},
{
year: "2024",
event: "Первый крупный проект",
description: "Успешный запуск платформы для блогов в ретро-стиле"
},
{
year: "2024",
event: "Расширение команды",
description: "К команде присоединились специалисты разных направлений"
},
{
year: "Настоящее время",
event: "Активное развитие",
description: "Работа над несколькими инновационными проектами одновременно"
}
]
}
},
technology: {
title: "Технологии",
icon: "",
content: {
main: "Мы используем современный стек технологий, сочетая его с проверенными временем решениями.",
categories: [
{
name: "Frontend",
technologies: ["Svelte/SvelteKit", "TypeScript", "Vue.js", "Tailwind CSS"],
color: "#00aaff"
},
{
name: "Backend",
technologies: ["Node.js", "Python", "Rust", "PostgreSQL", "MongoDB"],
color: "#ff00aa"
},
{
name: "DevOps",
technologies: ["Docker", "Kubernetes", "AWS", "GitLab CI/CD", "Linux"],
color: "#00ff88"
},
{
name: "Embedded",
technologies: ["C/C++", "Arduino", "Raspberry Pi", "IoT Protocols"],
color: "#ffaa00"
},
{
name: "QA",
technologies: ["Jest", "Cypress", "Selenium", "Test Automation"],
color: "#aa00ff"
}
]
}
}
};
const stats = {
projects: 12,
teamMembers: 5,
yearsExperience: 2,
technologies: 20,
satisfiedClients: 8,
codeCommits: 2500
};
onMount(() => {
isMobile.set(window.innerWidth < 768);
const handleResize = () => isMobile.set(window.innerWidth < 768);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
});
function setActiveSection(section) {
activeSection.set(section);
}
</script>
<section class="about-page">
<Row class=" g-0">
<!-- Левая панель - Навигация -->
<Col lg={3} class="h-100">
<div class="panel navigation-panel">
<CRTScreen class="navigation-terminal h-100">
<div class="terminal-header">
<span class="blink"></span> О КОМАНДЕ [58]
</div>
<div class="terminal-body navigation-body">
<div class="navigation-menu">
{#each Object.entries(teamData) as [key, section]}
<div
class="nav-item"
class:active={$activeSection === key}
on:click={() => setActiveSection(key)}
>
<span class="nav-icon">{section.icon}</span>
<span class="nav-title">{section.title}</span>
<span class="nav-arrow">
{#if $activeSection === key}
{:else}
{/if}
</span>
</div>
{/each}
</div>
<div class="quick-stats">
<h4> Быстрая статистика</h4>
<div class="stats-grid">
<div class="stat">
<span class="stat-number">{stats.projects}+</span>
<span class="stat-label">Проектов</span>
</div>
<div class="stat">
<span class="stat-number">{stats.teamMembers}</span>
<span class="stat-label">Участников</span>
</div>
<div class="stat">
<span class="stat-number">{stats.yearsExperience}</span>
<span class="stat-label">Года опыта</span>
</div>
<div class="stat">
<span class="stat-number">{stats.technologies}+</span>
<span class="stat-label">Технологий</span>
</div>
</div>
</div>
<div class="contact-info">
<h4> Контакты</h4>
<div class="contact-item">
<span class="contact-icon"></span>
<span class="contact-text">team58@example.com</span>
</div>
<div class="contact-item">
<span class="contact-icon"></span>
<span class="contact-text">@58team</span>
</div>
<div class="contact-item">
<span class="contact-icon"></span>
<span class="contact-text">github.com/58team</span>
</div>
</div>
</div>
</CRTScreen>
</div>
</Col>
<!-- Правая панель - Контент -->
<Col lg={9} class="h-100">
<div class="panel content-panel">
<CRTScreen class="content-terminal h-100">
<div class="terminal-header">
<span class="blink"></span>
{teamData[$activeSection].icon} {teamData[$activeSection].title}
</div>
<div class="terminal-body content-body">
<div class="content-scroll">
{#if $activeSection === 'mission'}
<div class="mission-content">
<div class="section-header">
<h1>Наша Миссия</h1>
<p class="lead">{teamData.mission.content.main}</p>
</div>
<div class="mission-points">
{#each teamData.mission.content.points as point}
<div class="mission-point">
<span class="point-icon"></span>
<span class="point-text">{point}</span>
</div>
{/each}
</div>
<div class="mission-vision">
<h2> Наше Видение</h2>
<div class="vision-grid">
<div class="vision-card">
<div class="vision-icon"></div>
<h3>Инновации</h3>
<p>Создавать технологии, которые опережают время</p>
</div>
<div class="vision-card">
<div class="vision-icon"></div>
<h3>Влияние</h3>
<p>Менять мир к лучшему через технологии</p>
</div>
<div class="vision-card">
<div class="vision-icon"></div>
<h3>Сообщество</h3>
<p>Развивать open-source и помогать другим</p>
</div>
</div>
</div>
</div>
{:else if $activeSection === 'values'}
<div class="values-content">
<div class="section-header">
<h1>Наши Ценности</h1>
<p class="lead">{teamData.values.content.main}</p>
</div>
<div class="values-grid">
{#each teamData.values.content.points as value}
<div class="value-card">
<div class="value-icon">{value.icon}</div>
<div class="value-content">
<h3>{value.title}</h3>
<p>{value.description}</p>
</div>
</div>
{/each}
</div>
<div class="values-principles">
<h2>Наши Принципы Работы</h2>
<div class="principles-list">
<div class="principle">
<strong>Agile подход:</strong> Гибкость и адаптивность в разработке
</div>
<div class="principle">
<strong>Code Review:</strong> Каждая строка кода проверяется коллегами
</div>
<div class="principle">
<strong>Документация:</strong> Подробная документация для всех проектов
</div>
<div class="principle">
<strong>Непрерывное обучение:</strong> Регулярные воркшопы и обмен знаниями
</div>
</div>
</div>
</div>
{:else if $activeSection === 'history'}
<div class="history-content">
<div class="section-header">
<h1>Наша История</h1>
<p class="lead">{teamData.history.content.main}</p>
</div>
<div class="timeline">
{#each teamData.history.content.timeline as event, index}
<div class="timeline-item">
<div class="timeline-marker">
<div class="marker-dot"></div>
{#if index < teamData.history.content.timeline.length - 1}
<div class="marker-line"></div>
{/if}
</div>
<div class="timeline-content">
<div class="timeline-year">{event.year}</div>
<h3>{event.event}</h3>
<p>{event.description}</p>
</div>
</div>
{/each}
</div>
<div class="history-achievements">
<h2> Наши Достижения</h2>
<div class="achievements-grid">
<div class="achievement">
<div class="achievement-icon"></div>
<div class="achievement-text">Успешный запуск 10+ проектов</div>
</div>
<div class="achievement">
<div class="achievement-icon"></div>
<div class="achievement-text">Собрана сильная команда специалистов</div>
</div>
<div class="achievement">
<div class="achievement-icon"></div>
<div class="achievement-text">Разработана уникальная ретро-платформа</div>
</div>
<div class="achievement">
<div class="achievement-icon"></div>
<div class="achievement-text">Постоянный рост и развитие</div>
</div>
</div>
</div>
</div>
{:else if $activeSection === 'technology'}
<div class="technology-content">
<div class="section-header">
<h1>Наш Технологический Стек</h1>
<p class="lead">{teamData.technology.content.main}</p>
</div>
<div class="tech-categories">
{#each teamData.technology.content.categories as category}
<div class="tech-category">
<h3 style="color: {category.color}">{category.name}</h3>
<div class="tech-tags">
{#each category.technologies as tech}
<span
class="tech-tag"
style="border-color: {category.color}; color: {category.color}"
>
{tech}
</span>
{/each}
</div>
</div>
{/each}
</div>
<div class="tech-philosophy">
<h2>Наш Подход к Технологиям</h2>
<div class="philosophy-points">
<div class="philosophy-point">
<strong>Правильный инструмент для задачи:</strong> Выбираем технологии исходя из требований проекта
</div>
<div class="philosophy-point">
<strong>Современные стандарты:</strong> Следуем best practices и актуальным трендам
</div>
<div class="philosophy-point">
<strong>Безопасность и надежность:</strong> Приоритет - стабильность и защита данных
</div>
<div class="philosophy-point">
<strong>Масштабируемость:</strong> Архитектура, которая растет вместе с проектом
</div>
</div>
</div>
</div>
{/if}
<div class="content-footer">
<div class="footer-cta">
<h3> Готовы работать с нами?</h3>
<p>Мы всегда открыты для новых вызовов и интересных проектов</p>
<div class="cta-buttons">
<a href="/contact" class="cta-btn primary">Связаться с нами</a>
<a href="/projects" class="cta-btn secondary">Наши проекты</a>
</div>
</div>
</div>
</div>
</div>
</CRTScreen>
</div>
</Col>
</Row>
</section>
<style>
.about-page {
min-height: 100vh;
padding-top: 50px;
background: var(--bg-dark);
overflow: hidden;
}
.panel {
height: 100%;
border: 1px solid var(--terminal-border);
background: rgba(10, 10, 10, 0.9);
}
.terminal-header {
background: var(--primary-green);
color: var(--bg-dark);
padding: 12px 20px;
font-weight: bold;
border-bottom: 2px solid var(--bg-dark);
font-size: 0.9rem;
font-family: 'Courier New', monospace;
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
}
.terminal-body {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
}
.navigation-body {
padding: 20px;
display: flex;
flex-direction: column;
gap: 30px;
}
.navigation-menu {
display: flex;
flex-direction: column;
gap: 5px;
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 15px;
border: 1px solid var(--terminal-border);
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
background: rgba(0, 0, 0, 0.2);
}
.nav-item:hover {
border-color: var(--primary-green);
transform: translateX(5px);
}
.nav-item.active {
border-color: var(--secondary-pink);
background: rgba(255, 0, 170, 0.1);
}
.nav-icon {
font-size: 1.2rem;
width: 20px;
text-align: center;
}
.nav-title {
flex: 1;
color: var(--text-secondary);
font-weight: bold;
}
.nav-item.active .nav-title {
color: var(--primary-green);
}
.nav-arrow {
color: var(--text-muted);
}
.quick-stats h4, .contact-info h4 {
color: var(--primary-green);
margin: 0 0 15px 0;
font-size: 1rem;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.stat {
background: rgba(0, 0, 0, 0.3);
border: 1px solid var(--terminal-border);
border-radius: 6px;
padding: 10px;
text-align: center;
}
.stat-number {
display: block;
color: var(--primary-green);
font-size: 1.2rem;
font-weight: bold;
margin-bottom: 2px;
}
.stat-label {
color: var(--text-muted);
font-size: 0.7rem;
}
.contact-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.contact-item:last-child {
border-bottom: none;
}
.contact-icon {
font-size: 1rem;
width: 20px;
text-align: center;
}
.contact-text {
color: var(--text-secondary);
font-size: 0.9rem;
}
/* Контентная часть */
.content-body {
padding: 0;
}
.content-scroll {
flex: 1;
padding: 30px;
overflow-y: auto;
}
.section-header {
margin-bottom: 40px;
text-align: center;
}
.section-header h1 {
color: var(--primary-green);
font-size: 2.5rem;
margin: 0 0 20px 0;
}
.lead {
color: var(--text-secondary);
font-size: 1.2rem;
line-height: 1.6;
max-width: 800px;
margin: 0 auto;
}
/* Стили для разных секций */
.mission-points {
display: flex;
flex-direction: column;
gap: 15px;
margin: 30px 0;
}
.mission-point {
display: flex;
align-items: center;
gap: 15px;
padding: 15px;
background: rgba(0, 0, 0, 0.3);
border: 1px solid var(--terminal-border);
border-radius: 8px;
}
.point-icon {
color: var(--primary-green);
font-weight: bold;
font-size: 1.2rem;
}
.point-text {
color: var(--text-secondary);
font-size: 1.1rem;
}
.vision-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-top: 30px;
}
.vision-card {
background: rgba(0, 0, 0, 0.3);
border: 1px solid var(--terminal-border);
border-radius: 8px;
padding: 20px;
text-align: center;
transition: all 0.3s ease;
}
.vision-card:hover {
border-color: var(--primary-green);
transform: translateY(-5px);
}
.vision-icon {
font-size: 2rem;
margin-bottom: 15px;
}
.vision-card h3 {
color: var(--primary-green);
margin: 0 0 10px 0;
}
.vision-card p {
color: var(--text-secondary);
margin: 0;
line-height: 1.5;
}
.values-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin: 30px 0;
}
.value-card {
display: flex;
gap: 15px;
padding: 20px;
background: rgba(0, 0, 0, 0.3);
border: 1px solid var(--terminal-border);
border-radius: 8px;
transition: all 0.3s ease;
}
.value-card:hover {
border-color: var(--primary-green);
}
.value-icon {
font-size: 1.5rem;
margin-top: 5px;
}
.value-content h3 {
color: var(--primary-green);
margin: 0 0 10px 0;
font-size: 1.2rem;
}
.value-content p {
color: var(--text-secondary);
margin: 0;
line-height: 1.5;
}
.principles-list {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 20px;
}
.principle {
color: var(--text-secondary);
padding: 10px 15px;
background: rgba(0, 0, 0, 0.2);
border-radius: 6px;
border-left: 3px solid var(--primary-green);
}
.timeline {
position: relative;
margin: 40px 0;
}
.timeline-item {
display: flex;
gap: 20px;
margin-bottom: 30px;
}
.timeline-marker {
display: flex;
flex-direction: column;
align-items: center;
}
.marker-dot {
width: 12px;
height: 12px;
background: var(--primary-green);
border-radius: 50%;
border: 2px solid var(--bg-dark);
}
.marker-line {
flex: 1;
width: 2px;
background: var(--primary-green);
margin-top: 5px;
}
.timeline-content {
flex: 1;
padding-bottom: 20px;
}
.timeline-year {
color: var(--secondary-pink);
font-weight: bold;
font-size: 1.1rem;
margin-bottom: 5px;
}
.timeline-content h3 {
color: var(--primary-green);
margin: 0 0 10px 0;
font-size: 1.3rem;
}
.timeline-content p {
color: var(--text-secondary);
margin: 0;
line-height: 1.5;
}
.achievements-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-top: 20px;
}
.achievement {
display: flex;
align-items: center;
gap: 10px;
padding: 15px;
background: rgba(0, 0, 0, 0.3);
border: 1px solid var(--terminal-border);
border-radius: 8px;
}
.achievement-icon {
font-size: 1.5rem;
}
.achievement-text {
color: var(--text-secondary);
font-size: 0.9rem;
}
.tech-categories {
display: flex;
flex-direction: column;
gap: 25px;
margin: 30px 0;
}
.tech-category h3 {
margin: 0 0 15px 0;
font-size: 1.3rem;
}
.tech-tags {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.tech-tag {
padding: 5px 12px;
border: 1px solid;
border-radius: 20px;
font-size: 0.9rem;
background: rgba(0, 0, 0, 0.3);
transition: all 0.3s ease;
}
.tech-tag:hover {
background: rgba(0, 0, 0, 0.5);
transform: translateY(-2px);
}
.philosophy-points {
display: flex;
flex-direction: column;
gap: 15px;
margin-top: 20px;
}
.philosophy-point {
color: var(--text-secondary);
padding: 15px;
background: rgba(0, 0, 0, 0.2);
border-radius: 8px;
border-left: 4px solid var(--primary-green);
}
.content-footer {
margin-top: 50px;
padding-top: 30px;
border-top: 1px solid var(--terminal-border);
}
.footer-cta {
text-align: center;
background: rgba(0, 0, 0, 0.3);
border: 1px solid var(--terminal-border);
border-radius: 12px;
padding: 30px;
}
.footer-cta h3 {
color: var(--primary-green);
margin: 0 0 10px 0;
}
.footer-cta p {
color: var(--text-secondary);
margin: 0 0 20px 0;
}
.cta-buttons {
display: flex;
gap: 15px;
justify-content: center;
}
.cta-btn {
padding: 12px 24px;
border: 2px solid;
border-radius: 6px;
text-decoration: none;
font-weight: bold;
transition: all 0.3s ease;
}
.cta-btn.primary {
background: var(--primary-green);
border-color: var(--primary-green);
color: var(--bg-dark);
}
.cta-btn.secondary {
background: transparent;
border-color: var(--secondary-pink);
color: var(--secondary-pink);
}
.cta-btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
}
/* Адаптивность */
@media (max-width: 768px) {
.about-page {
height: auto;
min-height: 100vh;
}
.content-scroll {
padding: 20px;
}
.section-header h1 {
font-size: 2rem;
}
.vision-grid, .values-grid {
grid-template-columns: 1fr;
}
.cta-buttons {
flex-direction: column;
}
.timeline-item {
flex-direction: column;
}
.timeline-marker {
flex-direction: row;
margin-bottom: 10px;
}
.marker-line {
width: 100%;
height: 2px;
margin: 0 10px;
}
}
</style>

View file

@ -0,0 +1,837 @@
<script>
import { onMount, beforeUpdate } from 'svelte';
import { writable } from 'svelte/store';
import { Container, Row, Col } from '@sveltestrap/sveltestrap';
import CRTScreen from '$lib/components/CRTScreen.svelte';
import { blogService } from '$lib/services/blogService.js';
import { getRelativeTime } from '$lib/utils/date.js';
// Реактивные переменные
const blogPosts = writable([]);
const blogMetadata = writable(null);
const loading = writable(true);
const loadingMore = writable(false);
const error = writable(null);
// Пагинация
const currentPage = writable(1);
const hasMore = writable(true);
const postsPerPage = 6;
const selectedFilters = writable({
category: 'all',
tag: null,
author: 'all'
});
const expandedSections = writable(new Set(['categories']));
const isMobile = writable(false);
onMount(async () => {
isMobile.set(window.innerWidth < 768);
await loadBlogData();
const handleResize = () => isMobile.set(window.innerWidth < 768);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
});
beforeUpdate(() => {
$selectedFilters;
currentPage.set(1);
hasMore.set(true);
});
// Загрузка данных блога
async function loadBlogData() {
try {
loading.set(true);
error.set(null);
blogPosts.set([]);
// Загружаем метаданные и первую страницу постов
const [metadata, posts] = await Promise.all([
blogService.getBlogMetadata(),
blogService.getBlogPosts({
limit: postsPerPage
})
]);
blogMetadata.set(metadata);
blogPosts.set(posts);
// Проверяем есть ли еще посты
hasMore.set(posts.length === postsPerPage);
} catch (err) {
console.error('Error loading blog data:', err);
error.set('Ошибка загрузки данных блога. Пожалуйста, попробуйте позже.');
} finally {
loading.set(false);
}
}
// Загрузка дополнительных постов
async function loadMorePosts() {
if ($loadingMore || !$hasMore) return;
try {
loadingMore.set(true);
const nextPage = $currentPage + 1;
const newPosts = await blogService.getBlogPosts({
limit: postsPerPage,
...buildApiFilters($selectedFilters)
});
if (newPosts.length > 0) {
blogPosts.update(posts => [...posts, ...newPosts]);
currentPage.set(nextPage);
hasMore.set(newPosts.length === postsPerPage);
} else {
hasMore.set(false);
}
} catch (err) {
console.error('Error loading more posts:', err);
error.set('Ошибка загрузки дополнительных статей.');
} finally {
loadingMore.set(false);
}
}
// Построение параметров для API
function buildApiFilters(filters) {
const apiFilters = {};
if (filters.category !== 'all') {
apiFilters.category = filters.category;
}
if (filters.tag) {
apiFilters.tag = filters.tag;
}
if (filters.author !== 'all') {
apiFilters.username = filters.author;
}
return apiFilters;
}
// Применение фильтров
async function applyFilter(type, value) {
selectedFilters.update(filters => {
const newFilters = { ...filters };
if (type === 'category') {
newFilters.category = value;
newFilters.tag = null;
} else if (type === 'tag') {
newFilters.tag = value;
} else if (type === 'author') {
newFilters.author = value;
}
return newFilters;
});
// Сбрасываем пагинацию и загружаем заново
currentPage.set(1);
hasMore.set(true);
await loadFilteredPosts();
}
// Загрузка отфильтрованных постов
async function loadFilteredPosts() {
try {
loading.set(true);
blogPosts.set([]);
const posts = await blogService.getBlogPosts({
limit: postsPerPage,
...buildApiFilters($selectedFilters)
});
blogPosts.set(posts);
hasMore.set(posts.length === postsPerPage);
} catch (err) {
console.error('Error loading filtered posts:', err);
error.set('Ошибка применения фильтров. Пожалуйста, попробуйте снова.');
} finally {
loading.set(false);
}
}
// Вспомогательные функции
function toggleSection(sectionName) {
expandedSections.update(set => {
const newSet = new Set(set);
newSet.has(sectionName) ? newSet.delete(sectionName) : newSet.add(sectionName);
return newSet;
});
}
function isExpanded(sectionName) {
return $expandedSections.has(sectionName);
}
function isActiveFilter(type, value) {
return $selectedFilters[type] === value;
}
function clearAllFilters() {
selectedFilters.set({
category: 'all',
tag: null,
author: 'all'
});
currentPage.set(1);
hasMore.set(true);
loadFilteredPosts();
}
// Генерация структуры tree из метаданных
$: filterTree = $blogMetadata ? {
name: "blog",
type: "directory",
children: [
{
name: "categories",
type: "directory",
children: [
{ name: "all", type: "file", count: $blogMetadata.totalPosts },
...$blogMetadata.categories.map(cat => ({
name: cat.name,
type: "file",
count: cat.count
}))
]
},
{
name: "tags",
type: "directory",
children: $blogMetadata.tags.map(tag => ({
name: tag.name,
type: "file",
count: tag.count
}))
},
{
name: "authors",
type: "directory",
children: [
{ name: "all", type: "file", count: $blogMetadata.totalPosts },
...$blogMetadata.authors.map(author => ({
name: author.name,
type: "file",
count: author.count
}))
]
}
]
} : null;
</script>
<section class="blog-page">
{#if $error}
<div class="error-banner">
<div class="error-content">
<span class="error-icon">⚠️</span>
<span class="error-message">{$error}</span>
<button class="retry-btn" on:click={loadBlogData}>Повторить</button>
</div>
</div>
{/if}
<Row class="h-100 g-0">
<!-- Левая панель - Tree меню фильтров -->
<Col lg={3} class="h-100">
<div class="panel filters-panel">
<CRTScreen class="filters-terminal h-100">
<div class="terminal-header">
<span class="blink"></span> ФИЛЬТРЫ БЛОГА
</div>
<div class="terminal-body filters-body">
{#if $loading && !$blogMetadata}
<div class="loading-state">
<div class="loading-spinner"></div>
<span>Загрузка фильтров...</span>
</div>
{:else if $blogMetadata}
<div class="filters-tree">
{#each filterTree.children as section}
<div class="tree-section">
<div
class="tree-item directory section-header"
on:click={() => toggleSection(section.name)}
>
<span class="folder-icon">
{isExpanded(section.name) ? '📂' : '📁'}
</span>
<span class="section-name">{section.name}/</span>
<span class="section-arrow">
{isExpanded(section.name) ? '▼' : '▶'}
</span>
</div>
{#if isExpanded(section.name)}
<div class="tree-children">
{#each section.children as filter}
<div
class="tree-item file filter-item"
class:active={isActiveFilter(
section.name === 'categories' ? 'category' :
section.name === 'tags' ? 'tag' : 'author',
filter.name
)}
on:click={() => applyFilter(
section.name === 'categories' ? 'category' :
section.name === 'tags' ? 'tag' : 'author',
filter.name
)}
>
<span class="file-icon">📄</span>
<span class="filter-name">{filter.name}</span>
<span class="filter-count">({filter.count})</span>
</div>
{/each}
</div>
{/if}
</div>
{/each}
</div>
<div class="active-filters">
<div class="filters-header">
<span class="filters-title">АКТИВНЫЕ ФИЛЬТРЫ:</span>
<button class="clear-all" on:click={clearAllFilters}>Сбросить все</button>
</div>
{#if $selectedFilters.category !== 'all'}
<div class="active-filter">
<span class="filter-type">Категория:</span>
<span class="filter-value">{$selectedFilters.category}</span>
<button
class="clear-filter"
on:click={() => applyFilter('category', 'all')}
>×</button>
</div>
{/if}
{#if $selectedFilters.tag}
<div class="active-filter">
<span class="filter-type">Тег:</span>
<span class="filter-value">{$selectedFilters.tag}</span>
<button
class="clear-filter"
on:click={() => applyFilter('tag', null)}
>×</button>
</div>
{/if}
{#if $selectedFilters.author !== 'all'}
<div class="active-filter">
<span class="filter-type">Автор:</span>
<span class="filter-value">{$selectedFilters.author}</span>
<button
class="clear-filter"
on:click={() => applyFilter('author', 'all')}
>×</button>
</div>
{/if}
</div>
{:else}
<div class="error-state">
<span class="error-icon"></span>
<span>Ошибка загрузки фильтров</span>
</div>
{/if}
</div>
</CRTScreen>
</div>
</Col>
<!-- Правая панель - Карточки статей -->
<Col lg={9} class="h-100">
<div class="panel articles-panel">
<CRTScreen class="articles-terminal h-100">
<div class="terminal-header">
<span class="blink"></span>
СТАТЬИ
{#if $loading}
<span class="loading-dots">
<span class="dot">.</span>
<span class="dot">.</span>
<span class="dot">.</span>
</span>
{/if}
</div>
<div class="terminal-body articles-body">
{#if $loading && $blogPosts.length === 0}
<div class="loading-state">
<div class="loading-spinner"></div>
<span>Загрузка статей из API...</span>
</div>
{:else if $blogPosts.length === 0}
<div class="no-articles">
<div class="no-articles-icon"></div>
<h3>Статьи не найдены</h3>
<p>Попробуйте изменить фильтры или проверьте позже</p>
<button class="retry-btn" on:click={clearAllFilters}>Показать все статьи</button>
</div>
{:else}
<div class="articles-container">
<div class="articles-grid">
{#each $blogPosts as post}
<article class="article-card">
<div class="card-header">
<h3 class="article-title">
<a href={`/blog/${post.id}`}>{post.title}</a>
</h3>
<div class="article-meta">
<span class="meta-item">
<span class="meta-icon"></span>
{getRelativeTime(post.updatedAt)}
</span>
<span class="meta-item">
<span class="meta-icon"></span>
{post.username}
</span>
<span class="meta-item">
<span class="meta-icon"></span>
{post.category}
</span>
</div>
</div>
<p class="article-description">{post.description}</p>
<div class="card-footer">
<div class="article-tags">
{#each post.tags as tag}
<span
class="tag"
on:click={() => applyFilter('tag', tag)}
>#{tag}</span>
{/each}
</div>
<a href={`/blog/${post.id}`} class="read-link">
ЧИТАТЬ СТАТЬЮ →
</a>
</div>
</article>
{/each}
</div>
<!-- Кнопка загрузить еще -->
{#if $hasMore}
<div class="load-more-container">
{#if $loadingMore}
<div class="loading-more">
<div class="loading-spinner small"></div>
<span>Загрузка дополнительных статей...</span>
</div>
{:else}
<button class="load-more-btn" on:click={loadMorePosts}>
<span class="btn-icon"></span>
ЗАГРУЗИТЬ ЕЩЕ
</button>
{/if}
</div>
{:else if $blogPosts.length > 0}
<div class="end-of-list">
<span class="end-icon"></span>
<span>Вы достигли конца! Загружено {$blogPosts.length} статей.</span>
</div>
{/if}
</div>
{/if}
</div>
</CRTScreen>
</div>
</Col>
</Row>
</section>
<style>
.blog-page {
min-height: 100vh;
padding-top: 50px;
background: var(--bg-dark);
}
.panel {
height: 100%;
border: 1px solid var(--terminal-border);
background: rgba(10, 10, 10, 0.9);
}
.terminal-header {
background: var(--primary-green);
color: var(--bg-dark);
padding: 12px 20px;
font-weight: bold;
border-bottom: 2px solid var(--bg-dark);
font-size: 0.9rem;
font-family: 'Courier New', monospace;
display: flex;
align-items: center;
gap: 10px;
}
.terminal-body {
flex: 1;
padding: 20px;
overflow: auto;
}
/* Tree меню фильтров */
.filters-tree {
font-family: 'JetBrains Mono', 'Courier New', monospace;
font-size: 0.9rem;
}
.tree-section {
margin-bottom: 15px;
}
.section-header {
cursor: pointer;
padding: 8px 12px;
border-radius: 5px;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
color: var(--primary-green);
font-weight: bold;
}
.section-header:hover {
background: rgba(51, 255, 0, 0.1);
}
.folder-icon {
font-size: 1rem;
}
.section-name {
flex: 1;
text-transform: uppercase;
}
.section-arrow {
color: var(--text-muted);
}
.tree-children {
margin-left: 25px;
margin-top: 5px;
}
.filter-item {
cursor: pointer;
padding: 6px 10px;
border-radius: 4px;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
color: var(--text-secondary);
margin-bottom: 2px;
}
.filter-item:hover {
background: rgba(51, 255, 0, 0.05);
color: var(--primary-green);
}
.filter-item.active {
background: rgba(51, 255, 0, 0.2);
color: var(--primary-green);
border-left: 3px solid var(--primary-green);
}
.filter-name {
flex: 1;
text-transform: lowercase;
}
.filter-count {
color: var(--text-muted);
font-size: 0.8rem;
}
.file-icon {
font-size: 0.9rem;
}
/* Активные фильтры */
.active-filters {
margin-top: 20px;
padding-top: 15px;
border-top: 1px solid var(--terminal-border);
}
.filters-title {
color: var(--text-muted);
font-size: 0.8rem;
margin-bottom: 10px;
text-transform: uppercase;
}
.active-filter {
display: flex;
align-items: center;
gap: 8px;
padding: 5px 10px;
background: rgba(51, 255, 0, 0.1);
border-radius: 4px;
margin-bottom: 5px;
font-size: 0.8rem;
}
.filter-type {
color: var(--text-muted);
}
.filter-value {
color: var(--primary-green);
flex: 1;
}
.clear-filter {
background: none;
border: none;
color: var(--secondary-pink);
cursor: pointer;
font-size: 1.2rem;
padding: 0;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.clear-filter:hover {
color: var(--primary-green);
}
/* Сетка статей */
.articles-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 20px;
padding: 10px 0;
}
.article-card {
background: rgba(0, 0, 0, 0.3);
border: 1px solid var(--terminal-border);
border-radius: 8px;
padding: 20px;
transition: all 0.3s ease;
position: relative;
}
.article-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 30px rgba(51, 255, 0, 0.1);
border-color: var(--primary-green);
}
.card-header {
margin-bottom: 15px;
}
.article-title {
margin: 0 0 10px 0;
}
.article-title a {
color: var(--primary-green);
text-decoration: none;
font-size: 1.2rem;
transition: all 0.3s ease;
}
.article-title a:hover {
color: var(--secondary-pink);
}
.article-meta {
display: flex;
gap: 15px;
font-size: 0.8rem;
color: var(--text-muted);
}
.meta-item {
display: flex;
align-items: center;
gap: 4px;
}
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.article-tags {
display: flex;
flex-wrap: wrap;
gap: 5px;
}
.tag {
background: rgba(255, 0, 170, 0.1);
color: var(--secondary-pink);
padding: 2px 6px;
border-radius: 3px;
font-size: 0.7rem;
cursor: pointer;
transition: all 0.3s ease;
border: 1px solid transparent;
}
.tag:hover {
background: rgba(255, 0, 170, 0.2);
border-color: var(--secondary-pink);
}
.read-link {
color: var(--secondary-pink);
text-decoration: none;
font-weight: bold;
font-size: 0.9rem;
transition: all 0.3s ease;
display: inline-block;
}
.read-link:hover {
color: var(--primary-green);
transform: translateX(5px);
}
/* Сообщение об отсутствии статей */
.no-articles {
text-align: center;
padding: 60px 20px;
color: var(--text-muted);
}
.no-articles-icon {
font-size: 3rem;
margin-bottom: 20px;
}
.no-articles h3 {
color: var(--primary-green);
margin-bottom: 10px;
}
/* Адаптивность */
@media (max-width: 992px) {
.articles-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {
.blog-page {
height: auto;
min-height: 100vh;
}
.filters-panel {
height: 300px;
}
.articles-panel {
height: auto;
}
.terminal-body {
padding: 15px;
}
.tree-children {
margin-left: 15px;
}
.article-card {
padding: 15px;
}
.card-footer {
flex-direction: column;
gap: 10px;
align-items: flex-start;
}
}
@media (max-width: 576px) {
.articles-grid {
grid-template-columns: 1fr;
}
.article-meta {
flex-direction: column;
gap: 5px;
}
.terminal-body {
padding: 10px;
}
}
.load-more-btn {
width: 100%;
padding: 15px;
background: rgba(51, 255, 0, 0.1);
border: 2px solid var(--primary-green);
color: var(--primary-green);
font-family: 'Courier New', monospace;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.load-more-btn:hover {
background: var(--primary-green);
color: var(--bg-dark);
transform: translateY(-2px);
}
.clear-all {
background: none;
border: 1px solid var(--secondary-pink);
color: var(--secondary-pink);
padding: 2px 8px;
border-radius: 3px;
font-size: 0.7rem;
cursor: pointer;
transition: all 0.3s ease;
}
.clear-all:hover {
background: var(--secondary-pink);
color: var(--bg-dark);
}
</style>

View file

@ -0,0 +1,5 @@
export async function load({ params }) {
return {
id: params.id
};
}

View file

@ -0,0 +1,657 @@
<script>
import { onMount } from 'svelte';
import { writable } from 'svelte/store';
import { Container, Row, Col } from '@sveltestrap/sveltestrap';
import CRTScreen from '$lib/components/CRTScreen.svelte';
import { blogService } from '$lib/services/blogService.js';
import { formatDateTime, getRelativeTime } from '$lib/utils/date.js';
export let data;
const { id } = data;
// Реактивные переменные
const post = writable(null);
const loading = writable(true);
const error = writable(null);
const relatedPosts = writable([]);
const isMobile = writable(false);
onMount(async () => {
isMobile.set(window.innerWidth < 768);
await loadPostData();
const handleResize = () => isMobile.set(window.innerWidth < 768);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
});
// Загрузка данных статьи
async function loadPostData() {
try {
loading.set(true);
error.set(null);
const postData = await blogService.getPostById(id);
if (!postData) {
throw new Error('Статья не найдена');
}
post.set(postData);
// Загружаем похожие статьи
await loadRelatedPosts(postData);
} catch (err) {
console.error('Error loading post:', err);
error.set('Статья не найдена или произошла ошибка загрузки.');
} finally {
loading.set(false);
}
}
// Загрузка похожих статей
async function loadRelatedPosts(currentPost) {
try {
const allPosts = await blogService.getBlogPosts();
// Фильтруем похожие статьи по категории и тегам
const related = allPosts
.filter(p => p.id !== currentPost.id) // Исключаем текущую статью
.filter(p =>
p.category === currentPost.category ||
(p.tags && currentPost.tags && p.tags.some(tag => currentPost.tags.includes(tag)))
)
.slice(0, 3); // Берем максимум 3 статьи
relatedPosts.set(related);
} catch (err) {
console.error('Error loading related posts:', err);
}
}
// Функция для парсинга тегов (если они приходят как строка)
function parseTags(tags) {
if (Array.isArray(tags)) {
return tags;
}
if (typeof tags === 'string') {
return tags.split(',').map(tag => tag.trim()).filter(tag => tag);
}
return [];
}
// Функция для форматирования контента (если нужно добавить разметку)
function formatContent(content) {
if (!content) return '';
// Простая обработка переносов строк
return content.replace(/\n/g, '<br>');
}
</script>
<section class="post-page">
{#if $error}
<div class="error-banner">
<div class="error-content">
<span class="error-icon">⚠️</span>
<span class="error-message">{$error}</span>
<button class="retry-btn" on:click={loadPostData}>Повторить</button>
<a href="/blog" class="back-btn">← Назад к статьям</a>
</div>
</div>
{/if}
<Row class="h-100 g-0">
<!-- Основной контент статьи -->
<Col lg={8} class="h-100">
<div class="panel post-panel">
<CRTScreen class="post-terminal h-100">
<div class="terminal-header">
<span class="blink"></span>
{#if $post}
ЧТЕНИЕ СТАТЬИ: {$post.title}
{:else}
ЗАГРУЗКА СТАТЬИ...
{/if}
</div>
<div class="terminal-body post-body">
{#if $loading}
<div class="loading-state">
<div class="loading-spinner"></div>
<span>Загрузка статьи...</span>
</div>
{:else if $post}
<div class="post-content">
<!-- Хедер статьи -->
<header class="post-header">
<nav class="breadcrumb">
<a href="/blog" class="breadcrumb-link">Блог</a>
<span class="breadcrumb-separator">/</span>
<span class="breadcrumb-current">{$post.title}</span>
</nav>
<h1 class="post-title">{$post.title}</h1>
<div class="post-meta">
<div class="meta-grid">
<div class="meta-item">
<span class="meta-icon"></span>
<span class="meta-label">Автор:</span>
<span class="meta-value">{$post.username}</span>
</div>
<div class="meta-item">
<span class="meta-icon"></span>
<span class="meta-label">Опубликовано:</span>
<span class="meta-value">{formatDateTime($post.createdAt)}</span>
</div>
<div class="meta-item">
<span class="meta-icon"></span>
<span class="meta-label">Обновлено:</span>
<span class="meta-value">{getRelativeTime($post.updatedAt)}</span>
</div>
<div class="meta-item">
<span class="meta-icon"></span>
<span class="meta-label">Категория:</span>
<span class="meta-value category-tag">{$post.category}</span>
</div>
</div>
</div>
{#if $post.tags && $post.tags.length > 0}
<div class="post-tags">
{#each parseTags($post.tags) as tag}
<span class="tag">#{tag}</span>
{/each}
</div>
{/if}
</header>
<!-- Описание статьи -->
{#if $post.description}
<div class="post-description">
<h3>📋 Описание</h3>
<p>{$post.description}</p>
</div>
{/if}
<!-- Основной контент -->
<article class="post-article">
<h3>📖 Содержание</h3>
<div class="content">{$post.content}</div>
</article>
<!-- Футер статьи -->
<footer class="post-footer">
<div class="footer-actions">
<a href="/blog" class="action-btn">
<span class="btn-icon"></span>
Назад к статьям
</a>
<button class="action-btn" on:click={() => window.scrollTo({ top: 0, behavior: 'smooth' })}>
<span class="btn-icon"></span>
Наверх
</button>
</div>
</footer>
</div>
{:else}
<div class="not-found">
<div class="not-found-icon"></div>
<h3>Статья не найдена</h3>
<p>Запрошенная статья не существует или была удалена.</p>
<a href="/blog" class="back-link">Вернуться к списку статей</a>
</div>
{/if}
</div>
</CRTScreen>
</div>
</Col>
<!-- Боковая панель с похожими статьями -->
<Col lg={4} class="h-100">
<div class="panel sidebar-panel">
<CRTScreen class="sidebar-terminal h-100">
<div class="terminal-header">
<span class="blink"></span> ПОХОЖИЕ СТАТЬИ
</div>
<div class="terminal-body sidebar-body">
{#if $relatedPosts.length > 0}
<div class="related-posts">
<h3>📚 Вам может понравиться</h3>
{#each $relatedPosts as relatedPost}
<article class="related-post-card">
<h4 class="related-title">
<a href="/blog/{relatedPost.id}">{relatedPost.title}</a>
</h4>
<div class="related-meta">
<span class="related-category">{relatedPost.category}</span>
<span class="related-date">{getRelativeTime(relatedPost.updatedAt)}</span>
</div>
{#if relatedPost.description}
<p class="related-description">{relatedPost.description}</p>
{/if}
<a href="/blog/{relatedPost.id}" class="related-link">
Читать далее →
</a>
</article>
{/each}
</div>
{:else}
<div class="no-related">
<span class="no-related-icon">📝</span>
<p>Нет похожих статей</p>
<a href="/blog" class="browse-link">Перейти ко всем статьям</a>
</div>
{/if}
<!-- Информация о статье -->
{#if $post}
<div class="post-info">
<h3> Информация</h3>
<div class="info-grid">
<div class="info-item">
<span class="info-label">ID статьи:</span>
<span class="info-value">{$post.id}</span>
</div>
<div class="info-item">
<span class="info-label">Автор ID:</span>
<span class="info-value">{$post.userId}</span>
</div>
<div class="info-item">
<span class="info-label">Создана:</span>
<span class="info-value">{formatDateTime($post.createdAt)}</span>
</div>
<div class="info-item">
<span class="info-label">Обновлена:</span>
<span class="info-value">{formatDateTime($post.updatedAt)}</span>
</div>
</div>
</div>
{/if}
</div>
</CRTScreen>
</div>
</Col>
</Row>
</section>
<style>
.post-page {
height: 100vh;
padding-top: 50px;
background: var(--bg-dark);
overflow: hidden;
}
.panel {
height: 100%;
border: 1px solid var(--terminal-border);
background: rgba(10, 10, 10, 0.9);
}
.terminal-header {
background: var(--primary-green);
color: var(--bg-dark);
padding: 12px 20px;
font-weight: bold;
border-bottom: 2px solid var(--bg-dark);
font-size: 0.9rem;
font-family: 'Courier New', monospace;
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
}
.terminal-body {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
}
.post-body {
padding: 0;
}
.post-content {
flex: 1;
padding: 30px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 30px;
}
.breadcrumb {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 20px;
font-size: 0.9rem;
color: var(--text-muted);
}
.breadcrumb-link {
color: var(--primary-green);
text-decoration: none;
transition: all 0.3s ease;
}
.breadcrumb-link:hover {
color: var(--secondary-pink);
}
.breadcrumb-separator {
color: var(--text-muted);
}
.breadcrumb-current {
color: var(--text-secondary);
}
.post-title {
color: var(--primary-green);
font-size: 2.2rem;
margin: 0 0 20px 0;
line-height: 1.3;
font-weight: bold;
}
.post-meta {
margin-bottom: 20px;
}
.meta-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 15px;
}
.meta-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.9rem;
}
.meta-icon {
font-size: 1rem;
}
.meta-label {
color: var(--text-muted);
font-weight: bold;
}
.meta-value {
color: var(--text-secondary);
}
.category-tag {
background: rgba(51, 255, 0, 0.1);
color: var(--primary-green);
padding: 2px 8px;
border-radius: 4px;
border: 1px solid var(--primary-green);
}
.post-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 20px;
}
.tag {
background: rgba(255, 0, 170, 0.1);
color: var(--secondary-pink);
padding: 4px 10px;
border-radius: 4px;
font-size: 0.8rem;
border: 1px solid var(--secondary-pink);
transition: all 0.3s ease;
}
.tag:hover {
background: rgba(255, 0, 170, 0.2);
transform: translateY(-1px);
}
.post-description {
background: rgba(0, 0, 0, 0.3);
border: 1px solid var(--terminal-border);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.post-description h3 {
color: var(--primary-green);
margin: 0 0 10px 0;
font-size: 1.2rem;
}
.post-description p {
color: var(--text-secondary);
line-height: 1.6;
margin: 0;
font-size: 1rem;
}
.post-article {
flex: 1;
}
.post-article h3 {
color: var(--primary-green);
margin: 0 0 20px 0;
font-size: 1.4rem;
}
.content {
color: var(--text-secondary);
line-height: 1.7;
font-size: 1.1rem;
}
.content br {
margin-bottom: 10px;
}
.post-footer {
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid var(--terminal-border);
}
.footer-actions {
display: flex;
gap: 15px;
justify-content: space-between;
}
.action-btn {
background: rgba(51, 255, 0, 0.1);
border: 1px solid var(--primary-green);
color: var(--primary-green);
padding: 10px 20px;
border-radius: 5px;
text-decoration: none;
font-family: 'Courier New', monospace;
font-weight: bold;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
.action-btn:hover {
background: var(--primary-green);
color: var(--bg-dark);
transform: translateY(-2px);
}
/* Боковая панель */
.sidebar-body {
padding: 20px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 30px;
}
.related-posts h3 {
color: var(--primary-green);
margin: 0 0 20px 0;
font-size: 1.2rem;
}
.related-post-card {
background: rgba(0, 0, 0, 0.3);
border: 1px solid var(--terminal-border);
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
transition: all 0.3s ease;
}
.related-post-card:hover {
border-color: var(--primary-green);
transform: translateX(5px);
}
.related-title {
margin: 0 0 10px 0;
}
.related-title a {
color: var(--primary-green);
text-decoration: none;
font-size: 1rem;
transition: all 0.3s ease;
}
.related-title a:hover {
color: var(--secondary-pink);
}
.related-meta {
display: flex;
justify-content: space-between;
font-size: 0.8rem;
color: var(--text-muted);
margin-bottom: 10px;
}
.related-description {
color: var(--text-secondary);
font-size: 0.9rem;
line-height: 1.4;
margin: 0 0 10px 0;
}
.related-link {
color: var(--secondary-pink);
text-decoration: none;
font-size: 0.9rem;
font-weight: bold;
transition: all 0.3s ease;
}
.related-link:hover {
color: var(--primary-green);
}
.no-related {
text-align: center;
color: var(--text-muted);
padding: 20px 0;
}
.no-related-icon {
font-size: 2rem;
margin-bottom: 10px;
display: block;
}
.browse-link {
color: var(--primary-green);
text-decoration: none;
margin-top: 10px;
display: inline-block;
}
.post-info {
background: rgba(0, 0, 0, 0.3);
border: 1px solid var(--terminal-border);
border-radius: 8px;
padding: 15px;
}
.post-info h3 {
color: var(--primary-green);
margin: 0 0 15px 0;
font-size: 1.1rem;
}
.info-grid {
display: flex;
flex-direction: column;
gap: 8px;
}
.info-item {
display: flex;
justify-content: space-between;
font-size: 0.8rem;
}
.info-label {
color: var(--text-muted);
}
.info-value {
color: var(--text-secondary);
font-family: 'Courier New', monospace;
}
/* Адаптивность */
@media (max-width: 768px) {
.post-page {
height: auto;
min-height: 100vh;
}
.post-content {
padding: 20px;
gap: 20px;
}
.post-title {
font-size: 1.8rem;
}
.meta-grid {
grid-template-columns: 1fr;
}
.footer-actions {
flex-direction: column;
}
.sidebar-body {
padding: 15px;
}
}
</style>

View file

@ -0,0 +1,827 @@
<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 teamMembers = [
{
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: "Идеального кода не существует, но к нему нужно стремиться"
}
];
const selectedMember = writable(teamMembers[0]);
const isMobile = writable(false);
const activeTab = writable('info');
onMount(() => {
isMobile.set(window.innerWidth < 768);
const handleResize = () => isMobile.set(window.innerWidth < 768);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
});
function selectMember(member) {
selectedMember.set(member);
activeTab.set('info');
}
function getRoleColor(role) {
const colors = {
'DevOps Engineer': '#00ff88',
'Embedded/Backend Developer': '#ff00aa',
'Frontend Developer': '#00aaff',
'CEO & Backend Developer': '#ffaa00',
'QA Engineer': '#aa00ff'
};
return colors[role] || '#00ff88';
}
</script>
<section class="team-page">
<Row class="h-100 g-0">
<!-- Левая панель - Список команды -->
<Col lg={4} class="h-100">
<div class="panel team-list-panel">
<CRTScreen class="team-list-terminal h-100">
<div class="terminal-header">
<span class="blink"></span> КОМАНДА [58]
</div>
<div class="terminal-body team-list-body">
<div class="team-stats">
<div class="stat-item">
<span class="stat-number">5</span>
<span class="stat-label">участников</span>
</div>
<div class="stat-item">
<span class="stat-number">2023</span>
<span class="stat-label">год основания</span>
</div>
<div class="stat-item">
<span class="stat-number">10+</span>
<span class="stat-label">проектов</span>
</div>
</div>
<div class="members-list">
{#each teamMembers as member}
<div
class="member-item"
class:active={$selectedMember.id === member.id}
on:click={() => selectMember(member)}
>
<div class="member-avatar">{member.avatar}</div>
<div class="member-info">
<div class="member-name">{member.name}</div>
<div class="member-username">@{member.username}</div>
<div
class="member-role"
style="color: {getRoleColor(member.role)}"
>
{member.role}
</div>
</div>
<div class="member-selector">
{#if $selectedMember.id === member.id}
<span class="selector-active"></span>
{:else}
<span class="selector-inactive"></span>
{/if}
</div>
</div>
{/each}
</div>
</CRTScreen>
</div>
</Col>
<!-- Правая панель - Детали участника -->
<Col lg={8} class="h-100">
<div class="panel member-detail-panel">
<CRTScreen class="member-detail-terminal h-100">
<div class="terminal-header">
<span class="blink"></span>
ПРОФИЛЬ:
</div>
<div class="terminal-body member-detail-body">
<div class="member-header">
<div class="avatar-section">
<div class="main-avatar">{$selectedMember.avatar}</div>
<div
class="role-badge"
style="border-color: {getRoleColor($selectedMember.role)}; color: {getRoleColor($selectedMember.role)}"
>
{$selectedMember.role}
</div>
</div>
<div class="basic-info">
<h1 class="member-name">{$selectedMember.name}</h1>
<div class="member-specialty">{$selectedMember.specialty}</div>
<div class="member-motto">"{$selectedMember.motto}"</div>
</div>
</div>
<!-- Навигация табов -->
<nav class="detail-tabs">
<button
class:active={$activeTab === 'info'}
class="tab-btn"
on:click={() => activeTab.set('info')}
>
Информация
</button>
<button
class:active={$activeTab === 'skills'}
class="tab-btn"
on:click={() => activeTab.set('skills')}
>
Навыки
</button>
<button
class:active={$activeTab === 'projects'}
class="tab-btn"
on:click={() => activeTab.set('projects')}
>
Проекты
</button>
</nav>
<!-- Контент табов -->
<div class="tab-content">
{#if $activeTab === 'info'}
<div class="info-tab">
<div class="info-section">
<h3>👤 О себе</h3>
<p>{$selectedMember.description}</p>
</div>
<div class="info-grid">
<div class="info-card">
<div class="info-icon"></div>
<div class="info-content">
<div class="info-label">Username</div>
<div class="info-value">@{$selectedMember.username}</div>
</div>
</div>
<div class="info-card">
<div class="info-icon"></div>
<div class="info-content">
<div class="info-label">В команде с</div>
<div class="info-value">{$selectedMember.joinDate}</div>
</div>
</div>
<div class="info-card">
<div class="info-icon"></div>
<div class="info-content">
<div class="info-label">Специализация</div>
<div class="info-value">{$selectedMember.specialty}</div>
</div>
</div>
<div class="info-card">
<div class="info-icon"></div>
<div class="info-content">
<div class="info-label">Девиз</div>
<div class="info-value">"{$selectedMember.motto}"</div>
</div>
</div>
</div>
</div>
{:else if $activeTab === 'skills'}
<div class="skills-tab">
<h3> Технические навыки</h3>
<div class="skills-grid">
{#each $selectedMember.skills as skill}
<div class="skill-item">
<span class="skill-name">{skill}</span>
<div class="skill-level">
<div
class="skill-progress"
style="width: {Math.floor(Math.random() * 30) + 70}%"
></div>
</div>
</div>
{/each}
</div>
<div class="skills-summary">
<h4>Сильные стороны</h4>
<ul>
<li>Глубокие знания в своей области</li>
<li>Способность быстро обучаться новым технологиям</li>
<li>Внимание к деталям и качеству</li>
<li>Командная работа и коммуникация</li>
</ul>
</div>
</div>
{:else if $activeTab === 'projects'}
<div class="projects-tab">
<h3>Ключевые проекты</h3>
<div class="projects-list">
{#each $selectedMember.projects as project}
<div class="project-card">
<div class="project-icon"></div>
<div class="project-content">
<h4>{project}</h4>
<p>Активный проект под руководством {$selectedMember.name}</p>
<div class="project-status">
<span class="status-badge active">В разработке</span>
<span class="project-role">Роль: {$selectedMember.role}</span>
</div>
</div>
</div>
{/each}
</div>
<div class="team-contribution">
<h4>Вклад в команду</h4>
<p>{$selectedMember.name} играет ключевую роль в успехе наших проектов, привнося экспертизу в области {$selectedMember.specialty.toLowerCase()}.</p>
</div>
</div>
{/if}
</div>
</div>
</CRTScreen>
</div>
</Col>
</Row>
</section>
<style>
.team-page {
min-height: 100vh;
padding-top: 50px;
background: var(--bg-dark);
overflow: hidden;
}
.panel {
height: 100%;
border: 1px solid var(--terminal-border);
background: rgba(10, 10, 10, 0.9);
}
.team-list-terminal, .member-detail-terminal {
height: 100%;
display: flex;
flex-direction: column;
}
.terminal-header {
background: var(--primary-green);
color: var(--bg-dark);
padding: 12px 20px;
font-weight: bold;
border-bottom: 2px solid var(--bg-dark);
font-size: 0.9rem;
font-family: 'Courier New', monospace;
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
}
.member-username {
color: var(--secondary-pink);
font-weight: normal;
}
.terminal-body {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
}
.team-list-body {
padding: 20px;
display: flex;
flex-direction: column;
gap: 20px;
}
.team-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
margin-bottom: 10px;
}
.stat-item {
background: rgba(0, 0, 0, 0.3);
border: 1px solid var(--terminal-border);
border-radius: 8px;
padding: 15px;
text-align: center;
}
.stat-number {
display: block;
color: var(--primary-green);
font-size: 1.5rem;
font-weight: bold;
margin-bottom: 5px;
}
.stat-label {
color: var(--text-muted);
font-size: 0.8rem;
}
.members-list {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 10px;
}
.member-item {
display: flex;
align-items: center;
gap: 15px;
padding: 15px;
border: 1px solid var(--terminal-border);
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
background: rgba(0, 0, 0, 0.2);
}
.member-item:hover {
border-color: var(--primary-green);
transform: translateX(5px);
}
.member-item.active {
border-color: var(--secondary-pink);
background: rgba(255, 0, 170, 0.1);
}
.member-avatar {
font-size: 2rem;
width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.3);
border-radius: 50%;
}
.member-info {
flex: 1;
}
.member-name {
color: var(--primary-green);
font-weight: bold;
font-size: 1.1rem;
margin-bottom: 2px;
}
.member-username {
color: var(--text-muted);
font-size: 0.8rem;
margin-bottom: 5px;
}
.member-role {
font-size: 0.8rem;
font-weight: bold;
}
.member-selector {
width: 20px;
text-align: center;
}
.selector-active {
color: var(--secondary-pink);
font-size: 1.2rem;
}
.selector-inactive {
color: var(--text-muted);
font-size: 0.8rem;
}
.team-description {
background: rgba(0, 0, 0, 0.3);
border: 1px solid var(--terminal-border);
border-radius: 8px;
padding: 15px;
margin-top: 10px;
}
.team-description h4 {
color: var(--primary-green);
margin: 0 0 10px 0;
font-size: 1rem;
}
.team-description p {
color: var(--text-secondary);
font-size: 0.9rem;
line-height: 1.4;
margin: 0;
}
/* Детали участника */
.member-detail-body {
padding: 0;
display: flex;
flex-direction: column;
}
.member-header {
display: flex;
gap: 30px;
padding: 30px;
border-bottom: 1px solid var(--terminal-border);
background: rgba(0, 0, 0, 0.2);
}
.avatar-section {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
.main-avatar {
font-size: 4rem;
width: 100px;
height: 100px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.3);
border-radius: 50%;
border: 3px solid var(--primary-green);
}
.role-badge {
padding: 5px 10px;
border: 2px solid;
border-radius: 20px;
font-size: 0.8rem;
font-weight: bold;
background: rgba(0, 0, 0, 0.5);
}
.basic-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
.member-name {
color: var(--primary-green);
font-size: 2.5rem;
font-weight: bold;
margin: 0 0 10px 0;
}
.member-specialty {
color: var(--text-secondary);
font-size: 1.2rem;
margin-bottom: 15px;
}
.member-motto {
color: var(--secondary-pink);
font-style: italic;
font-size: 1rem;
border-left: 3px solid var(--secondary-pink);
padding-left: 15px;
}
.detail-tabs {
display: flex;
border-bottom: 1px solid var(--terminal-border);
background: rgba(0, 0, 0, 0.3);
}
.tab-btn {
flex: 1;
padding: 15px 20px;
background: none;
border: none;
color: var(--text-secondary);
font-family: 'Courier New', monospace;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
border-bottom: 3px solid transparent;
}
.tab-btn:hover {
color: var(--primary-green);
background: rgba(51, 255, 0, 0.1);
}
.tab-btn.active {
color: var(--primary-green);
border-bottom-color: var(--primary-green);
background: rgba(51, 255, 0, 0.05);
}
.tab-content {
flex: 1;
padding: 30px;
overflow-y: auto;
}
.info-tab, .skills-tab, .projects-tab {
height: 100%;
}
.info-tab h3, .skills-tab h3, .projects-tab h3 {
color: var(--primary-green);
margin: 0 0 20px 0;
font-size: 1.4rem;
}
.info-section {
margin-bottom: 30px;
}
.info-section p {
color: var(--text-secondary);
line-height: 1.6;
font-size: 1.1rem;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
}
.info-card {
background: rgba(0, 0, 0, 0.3);
border: 1px solid var(--terminal-border);
border-radius: 8px;
padding: 20px;
display: flex;
align-items: center;
gap: 15px;
transition: all 0.3s ease;
}
.info-card:hover {
border-color: var(--primary-green);
transform: translateY(-2px);
}
.info-icon {
font-size: 1.5rem;
}
.info-content {
flex: 1;
}
.info-label {
color: var(--text-muted);
font-size: 0.8rem;
margin-bottom: 5px;
}
.info-value {
color: var(--text-secondary);
font-weight: bold;
}
.skills-grid {
display: flex;
flex-direction: column;
gap: 15px;
margin-bottom: 30px;
}
.skill-item {
display: flex;
align-items: center;
gap: 15px;
}
.skill-name {
color: var(--text-secondary);
font-weight: bold;
min-width: 120px;
}
.skill-level {
flex: 1;
height: 8px;
background: rgba(0, 0, 0, 0.3);
border-radius: 4px;
overflow: hidden;
}
.skill-progress {
height: 100%;
background: var(--primary-green);
border-radius: 4px;
transition: width 0.3s ease;
}
.skills-summary ul {
color: var(--text-secondary);
padding-left: 20px;
line-height: 1.6;
}
.projects-list {
display: flex;
flex-direction: column;
gap: 15px;
margin-bottom: 30px;
}
.project-card {
background: rgba(0, 0, 0, 0.3);
border: 1px solid var(--terminal-border);
border-radius: 8px;
padding: 20px;
display: flex;
gap: 15px;
transition: all 0.3s ease;
}
.project-card:hover {
border-color: var(--primary-green);
}
.project-icon {
font-size: 1.5rem;
}
.project-content {
flex: 1;
}
.project-content h4 {
color: var(--primary-green);
margin: 0 0 10px 0;
}
.project-content p {
color: var(--text-secondary);
margin: 0 0 10px 0;
font-size: 0.9rem;
}
.project-status {
display: flex;
gap: 15px;
align-items: center;
}
.status-badge {
padding: 3px 8px;
border-radius: 4px;
font-size: 0.7rem;
font-weight: bold;
}
.status-badge.active {
background: rgba(51, 255, 0, 0.2);
color: var(--primary-green);
border: 1px solid var(--primary-green);
}
.project-role {
color: var(--text-muted);
font-size: 0.8rem;
}
.team-contribution p {
color: var(--text-secondary);
line-height: 1.6;
}
.member-footer {
padding: 20px 30px;
border-top: 1px solid var(--terminal-border);
background: rgba(0, 0, 0, 0.3);
}
.footer-note {
display: flex;
align-items: center;
gap: 10px;
color: var(--text-muted);
font-size: 0.9rem;
}
.note-icon {
font-size: 1.2rem;
}
/* Адаптивность */
@media (max-width: 768px) {
.team-page {
height: auto;
min-height: 100vh;
}
.member-header {
flex-direction: column;
text-align: center;
gap: 20px;
padding: 20px;
}
.member-name {
font-size: 2rem;
}
.detail-tabs {
flex-direction: column;
}
.info-grid {
grid-template-columns: 1fr;
}
.tab-content {
padding: 20px;
}
}
</style>

View file

@ -0,0 +1,76 @@
import { json } from '@sveltejs/kit';
const mockTeam = [
{
id: 1,
name: 'Иван Иванов',
position: 'Senior Frontend Developer',
avatar: '/images/team/ivanov.jpg',
bio: 'Опытный разработчик с 5+ годами опыта. Специализируется на Svelte и React.',
skills: ['Svelte', 'React', 'TypeScript', 'Node.js'],
social: {
github: 'ivanov',
twitter: 'ivanov_dev',
linkedin: 'ivanov'
},
joined: '2022-01-15'
},
{
id: 2,
name: 'Петр Петров',
position: 'Fullstack Developer',
avatar: '/images/team/petrov.jpg',
bio: 'Fullstack разработчик с опытом работы над высоконагруженными системами.',
skills: ['Python', 'Django', 'Vue.js', 'PostgreSQL'],
social: {
github: 'petrov',
twitter: 'petrov_dev',
linkedin: 'petrov'
},
joined: '2022-03-10'
},
{
id: 3,
name: 'Мария Сидорова',
position: 'UI/UX Designer',
avatar: '/images/team/sidorova.jpg',
bio: 'Создаю интерфейсы, которые нравятся пользователям и решают бизнес-задачи.',
skills: ['Figma', 'Adobe XD', 'Photoshop', 'Illustrator'],
social: {
github: 'sidorova',
twitter: 'sidorova_design',
linkedin: 'sidorova'
},
joined: '2022-05-20'
},
{
id: 4,
name: 'Алексей Смирнов',
position: 'DevOps Engineer',
avatar: '/images/team/smirnov.jpg',
bio: 'Автоматизирую процессы разработки и deployment.',
skills: ['Docker', 'Kubernetes', 'AWS', 'CI/CD'],
social: {
github: 'smirnov',
twitter: 'smirnov_ops',
linkedin: 'smirnov'
},
joined: '2022-08-05'
}
];
export async function GET({ url }) {
const id = url.searchParams.get('id');
if (id) {
const member = mockTeam.find(m => m.id === parseInt(id));
return member ? json(member) : json({ error: 'Member not found' }, { status: 404 });
}
const limit = parseInt(url.searchParams.get('limit')) || 10;
const page = parseInt(url.searchParams.get('page')) || 1;
const paginatedTeam = mockTeam.slice((page - 1) * limit, page * limit);
return json(paginatedTeam);
}

23
src/routes/+error.svelte Normal file
View file

@ -0,0 +1,23 @@
<script>
import { Button } from '@sveltestrap/sveltestrap';
import { page } from '$app/stores';
</script>
<div class="container text-center mt-5">
<div class="row">
<div class="col-lg-6 mx-auto">
<i class="bi bi-exclamation-triangle display-1 text-warning"></i>
<h1 class="display-4">{$page.status}</h1>
<p class="lead">{$page.error?.message || 'Что-то пошло не так'}</p>
<div class="mt-4">
<Button color="primary" href="/" class="me-3">
<i class="bi bi-house"></i> На главную
</Button>
<Button color="secondary" on:click={() => location.reload()}>
<i class="bi bi-arrow-clockwise"></i> Обновить
</Button>
</div>
</div>
</div>
</div>

18
src/routes/+layout.svelte Normal file
View file

@ -0,0 +1,18 @@
<script>
import Footer from '$lib/components/Footer.svelte';
import Navigation from '$lib/components/Navigation.svelte';
import '../app.css';
</script>
<Navigation />
<main>
<slot />
</main>
<Footer/>
<style>
main {
min-height: 60vh;
}
</style>

View file

@ -0,0 +1,27 @@
import api from '$lib/utils/api';
export const teamService = {
getTeamMembers: async (params = {}) => {
try {
const queryParams = new URLSearchParams();
if (params.limit) queryParams.append('limit', params.limit);
if (params.page) queryParams.append('page', params.page);
const url = `/team?${queryParams.toString()}`;
return await api.get(url);
} catch (error) {
console.error('Error fetching team:', error);
return [];
}
},
getTeamMember: async (id) => {
try {
return await api.get(`/team?id=${id}`);
} catch (error) {
console.error('Error fetching team member:', error);
throw error;
}
}
};

View file

@ -0,0 +1,77 @@
import { json } from '@sveltejs/kit';
const mockPosts = [
{
id: 1,
slug: 'sveltekit-introduction',
title: 'Введение в SvelteKit',
content: `# Введение в SvelteKit
SvelteKit - это фреймворк для построения веб-приложений на Svelte.
## Основные преимущества
- **Производительность** - нет рантайма
- **SEO-friendly** - SSR из коробки
- **Простота** - минимальный boilerplate
## Установка
\`\`\`bash
npm create svelte@latest my-app
\`\`\`
`,
excerpt: 'Знакомство с фреймворком SvelteKit и его основными возможностями',
date: '2024-01-15',
author: 'Иван Иванов',
tags: ['svelte', 'javascript', 'frontend']
},
{
id: 2,
slug: 'bootstrap-svelte',
title: 'Bootstrap в Svelte проектах',
content: `# Использование Bootstrap с Svelte
Bootstrap отлично работает с Svelte через sveltestrap.
## Установка
\`\`\`bash
npm install @sveltestrap/sveltestrap
\`\`\`
## Использование
Просто импортируйте нужные компоненты.`,
excerpt: 'Как интегрировать Bootstrap в Svelte приложение',
date: '2024-01-10',
author: 'Петр Петров',
tags: ['bootstrap', 'ui', 'frontend']
}
];
export async function GET({ url }) {
try {
const slug = url.searchParams.get('slug');
if (slug) {
const post = mockPosts.find(p => p.slug === slug);
if (!post) {
return json({ error: 'Post not found' }, { status: 404 });
}
return json(post);
}
const limit = parseInt(url.searchParams.get('limit')) || 10;
const page = parseInt(url.searchParams.get('page')) || 1;
const offset = (page - 1) * limit;
const paginatedPosts = mockPosts.slice(offset, offset + limit);
return json(paginatedPosts);
} catch (error) {
return json({ error: 'Internal server error' }, { status: 500 });
}
}

View file

@ -0,0 +1,57 @@
import { json } from '@sveltejs/kit';
const mockTeam = [
{
id: 1,
name: 'Иван Иванов',
position: 'Senior Frontend Developer',
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face',
bio: 'Опытный разработчик с 5+ годами опыта. Специализируется на Svelte и React.',
skills: ['Svelte', 'React', 'TypeScript', 'Node.js'],
social: {
github: 'ivanov',
twitter: 'ivanov_dev',
linkedin: 'ivanov'
},
joined: '2022-01-15'
},
{
id: 2,
name: 'Петр Петров',
position: 'Fullstack Developer',
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&h=150&fit=crop&crop=face',
bio: 'Fullstack разработчик с опытом работы над высоконагруженными системами.',
skills: ['Python', 'Django', 'Vue.js', 'PostgreSQL'],
social: {
github: 'petrov',
twitter: 'petrov_dev',
linkedin: 'petrov'
},
joined: '2022-03-10'
}
];
export async function GET({ url }) {
try {
const id = url.searchParams.get('id');
if (id) {
const member = mockTeam.find(m => m.id === parseInt(id));
if (!member) {
return json({ error: 'Member not found' }, { status: 404 });
}
return json(member);
}
const limit = parseInt(url.searchParams.get('limit')) || 10;
const page = parseInt(url.searchParams.get('page')) || 1;
const offset = (page - 1) * limit;
const paginatedTeam = mockTeam.slice(offset, offset + limit);
return json(paginatedTeam);
} catch (error) {
return json({ error: 'Internal server error' }, { status: 500 });
}
}

1
static/favicon.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

14
svelte.config.js Normal file
View file

@ -0,0 +1,14 @@
import { mdsvex } from 'mdsvex';
import adapter from '@sveltejs/adapter-auto';
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: { // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
adapter: adapter() },
preprocess: [mdsvex()],
extensions: ['.svelte', '.svx']
};
export default config;

6
vite.config.js Normal file
View file

@ -0,0 +1,6 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()]
});