aaaaa
This commit is contained in:
commit
5ab2d8abfd
45 changed files with 9738 additions and 0 deletions
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal 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
1
.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
engine-strict=true
|
||||
58
README.md
Normal file
58
README.md
Normal 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
6
builder.config.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"command": "npm run dev",
|
||||
"serverUrl": "http://localhost:5173/blog",
|
||||
"authenticateProxy": false,
|
||||
"commitMode": "commits"
|
||||
}
|
||||
12
jsconfig.json
Normal file
12
jsconfig.json
Normal 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
2235
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
51
package.json
Normal file
51
package.json
Normal 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
215
src/app.css
Normal 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
19
src/app.html
Normal 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>
|
||||
72
src/lib/components/BlogCard.svelte
Normal file
72
src/lib/components/BlogCard.svelte
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
<script>
|
||||
import GlitchText from './GlitchText.svelte';
|
||||
import CRTScreen from './CRTScreen.svelte';
|
||||
import { formatDate } from '../utils/date.js';
|
||||
|
||||
export let post = {
|
||||
slug: '',
|
||||
title: '',
|
||||
excerpt: '',
|
||||
date: '',
|
||||
author: ''
|
||||
};
|
||||
</script>
|
||||
|
||||
<CRTScreen class="blog-card">
|
||||
<div class="card-content">
|
||||
<h3 class="card-title">
|
||||
<GlitchText text={post.title} size="1.2rem" />
|
||||
</h3>
|
||||
|
||||
<p class="card-excerpt">{post.excerpt}</p>
|
||||
|
||||
<div class="card-meta">
|
||||
<span class="card-date">{formatDate(post.date)}</span>
|
||||
<span class="card-author">by {post.author}</span>
|
||||
</div>
|
||||
|
||||
<div class="card-actions">
|
||||
<a href={`/blog/${post.slug}`} class="read-more">Читать →</a>
|
||||
</div>
|
||||
</div>
|
||||
</CRTScreen>
|
||||
|
||||
<style>
|
||||
.blog-card {
|
||||
margin-bottom: 20px;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.blog-card:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
margin: 0 0 10px 0;
|
||||
color: #33ff00;
|
||||
}
|
||||
|
||||
.card-excerpt {
|
||||
color: #ccc;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.card-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.9rem;
|
||||
color: #888;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.read-more {
|
||||
color: #ff00ff;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.read-more:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
76
src/lib/components/CRTScreen.svelte
Normal file
76
src/lib/components/CRTScreen.svelte
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
<script>
|
||||
export let scanlines = true;
|
||||
export let flicker = true;
|
||||
</script>
|
||||
|
||||
<div class="crt-screen" class:no-scanlines={!scanlines} class:no-flicker={!flicker}>
|
||||
<div class="content">
|
||||
<slot />
|
||||
</div>
|
||||
<div class="scanlines"></div>
|
||||
<div class="glow"></div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.crt-screen {
|
||||
position: relative;
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 0 20px rgba(51, 255, 0, 0.3);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.scanlines {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(255, 255, 255, 0.03) 50%,
|
||||
rgba(0, 0, 0, 0.1) 50%
|
||||
);
|
||||
background-size: 100% 4px;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.glow {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
left: -10px;
|
||||
right: -10px;
|
||||
bottom: -10px;
|
||||
background: radial-gradient(
|
||||
circle at center,
|
||||
rgba(51, 255, 0, 0.1) 0%,
|
||||
transparent 70%
|
||||
);
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.no-scanlines .scanlines {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.no-flicker {
|
||||
animation: flicker 0.15s infinite;
|
||||
}
|
||||
|
||||
@keyframes flicker {
|
||||
0% { opacity: 0.9; }
|
||||
5% { opacity: 0.8; }
|
||||
10% { opacity: 0.9; }
|
||||
15% { opacity: 1; }
|
||||
20% { opacity: 0.9; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
7
src/lib/components/Error.svelte
Normal file
7
src/lib/components/Error.svelte
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<script>
|
||||
import { Alert } from '@sveltestrap/sveltestrap';
|
||||
</script>
|
||||
|
||||
<Alert color="danger" class="my-3">
|
||||
{message}
|
||||
</Alert>
|
||||
104
src/lib/components/Footer.svelte
Normal file
104
src/lib/components/Footer.svelte
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
const systemStatus = writable('OPERATIONAL');
|
||||
const uptime = writable('0d 0h 0m');
|
||||
|
||||
onMount(() => {
|
||||
const startTime = Date.now();
|
||||
|
||||
function updateUptime() {
|
||||
const diff = Date.now() - startTime;
|
||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
||||
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
||||
uptime.set(`${days}d ${hours}h ${minutes}m`);
|
||||
}
|
||||
|
||||
updateUptime();
|
||||
const interval = setInterval(updateUptime, 60000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
});
|
||||
</script>
|
||||
|
||||
<footer class="minimal-footer">
|
||||
<div class="footer-container">
|
||||
<div class="footer-left">
|
||||
<span class="footer-text">[58] Team © 2024</span>
|
||||
<span class="footer-separator">|</span>
|
||||
<span class="footer-status">
|
||||
<span class="status-indicator"></span>
|
||||
{$systemStatus}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="footer-right">
|
||||
<span class="footer-uptime">UPTIME: {$uptime}</span>
|
||||
<span class="footer-separator">|</span>
|
||||
<span class="footer-version">v1.0.0</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
.minimal-footer {
|
||||
background: rgba(10, 10, 10, 0.95);
|
||||
border-top: 1px solid var(--terminal-border);
|
||||
padding: 8px 20px;
|
||||
font-family: 'JetBrains Mono', 'Fira Code', 'Courier New', monospace;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.footer-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.footer-left, .footer-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.footer-separator {
|
||||
color: var(--terminal-border);
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: var(--primary-green);
|
||||
border-radius: 50%;
|
||||
margin-right: 6px;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.minimal-footer {
|
||||
padding: 6px 10px;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.footer-container {
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer-left, .footer-right {
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
58
src/lib/components/GlitchText.svelte
Normal file
58
src/lib/components/GlitchText.svelte
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
<script>
|
||||
export let text = '';
|
||||
export let glitch = true;
|
||||
export let size = '1rem';
|
||||
</script>
|
||||
|
||||
<div class="glitch-text" class:no-glitch={!glitch} style="font-size: {size}">
|
||||
<span class="glitch" data-text="{text}">{text}</span>
|
||||
<span class="glitch glitch-2" data-text="{text}">{text}</span>
|
||||
<span class="glitch glitch-3" data-text="{text}">{text}</span>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.glitch-text {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.glitch {
|
||||
position: relative;
|
||||
color: #fff;
|
||||
text-shadow:
|
||||
0.05em 0 0 rgba(255, 0, 0, 0.75),
|
||||
-0.05em -0.025em 0 rgba(0, 255, 0, 0.75),
|
||||
0.025em 0.05em 0 rgba(0, 0, 255, 0.75);
|
||||
animation: glitch 2s infinite;
|
||||
}
|
||||
|
||||
.glitch-2 {
|
||||
animation: glitch-2 3s infinite;
|
||||
opacity: 0.8;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.glitch-3 {
|
||||
animation: glitch-3 4s infinite;
|
||||
opacity: 0.6;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
@keyframes glitch {
|
||||
0% { transform: translate(0); }
|
||||
20% { transform: translate(-2px, 2px); }
|
||||
40% { transform: translate(-2px, -2px); }
|
||||
60% { transform: translate(2px, 2px); }
|
||||
80% { transform: translate(2px, -2px); }
|
||||
100% { transform: translate(0); }
|
||||
}
|
||||
|
||||
.no-glitch .glitch {
|
||||
animation: none;
|
||||
text-shadow: none;
|
||||
}
|
||||
</style>
|
||||
7
src/lib/components/Loading.svelte
Normal file
7
src/lib/components/Loading.svelte
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<script>
|
||||
import { Spinner } from '@sveltestrap/sveltestrap';
|
||||
</script>
|
||||
|
||||
<div class="text-center my-5">
|
||||
<Spinner color="primary" />
|
||||
</div>
|
||||
9
src/lib/components/MarkdownRender.svelte
Normal file
9
src/lib/components/MarkdownRender.svelte
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<script>
|
||||
import { marked } from 'marked';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
export let content = '';
|
||||
|
||||
$: html = sanitizeHtml(marked.parse(content || ''));
|
||||
</script>
|
||||
|
||||
<div class="prose lg:prose-xl" bind:this={el} {@html html}></div>
|
||||
257
src/lib/components/Navigation.svelte
Normal file
257
src/lib/components/Navigation.svelte
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
const currentPath = writable('/');
|
||||
const currentLanguage = writable('EN');
|
||||
const isMobile = writable(false);
|
||||
|
||||
const paths = [
|
||||
{ href: '/', label: '~', icon: '🏠' },
|
||||
{ href: '/blog', label: 'blog', icon: '📝' },
|
||||
{ href: '/team', label: 'team', icon: '👥' },
|
||||
{ href: '/about', label: 'about', icon: 'ℹ️' },
|
||||
];
|
||||
|
||||
const languages = ['EN', 'RU', 'DE'];
|
||||
|
||||
onMount(() => {
|
||||
isMobile.set(window.innerWidth < 768);
|
||||
const handleResize = () => isMobile.set(window.innerWidth < 768);
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
function setLanguage(lang) {
|
||||
currentLanguage.set(lang);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
function updateTime() {
|
||||
const timeElement = document.getElementById('current-time');
|
||||
if (timeElement) {
|
||||
timeElement.textContent = new Date().toLocaleTimeString('en-US', {
|
||||
hour12: false,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updateTime();
|
||||
const interval = setInterval(updateTime, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
});
|
||||
</script>
|
||||
|
||||
<header class="waybar-header">
|
||||
<div class="waybar-container">
|
||||
<!-- Левая часть - Пути -->
|
||||
<nav class="path-navigation">
|
||||
{#each paths as path}
|
||||
<a
|
||||
href={path.href}
|
||||
class:active={$currentPath === path.href}
|
||||
class="path-item"
|
||||
>
|
||||
<span class="path-icon">{path.icon}</span>
|
||||
<span class="path-label">{path.label}</span>
|
||||
</a>
|
||||
{/each}
|
||||
</nav>
|
||||
|
||||
<!-- Центр - Логотип -->
|
||||
<div class="waybar-center">
|
||||
<div class="logo-container">
|
||||
<span class="logo-brackets">[</span>
|
||||
<span class="logo-number">58</span>
|
||||
<span class="logo-brackets">]</span>
|
||||
<span class="logo-text">Team</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Правая часть - Язык и время -->
|
||||
<div class="waybar-right">
|
||||
<div class="language-selector">
|
||||
<button
|
||||
class="language-btn"
|
||||
on:click={() => setLanguage(languages[(languages.indexOf($currentLanguage) + 1) % languages.length])}
|
||||
>
|
||||
<span class="language-flag">🌐</span>
|
||||
<span class="language-code">{$currentLanguage}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="time-display">
|
||||
<span class="time" id="current-time">
|
||||
{new Date().toLocaleTimeString('en-US', { hour12: false })}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<style>
|
||||
.waybar-header {
|
||||
background: rgba(10, 10, 10, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid var(--primary-green);
|
||||
height: 50px;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
font-family: 'JetBrains Mono', 'Fira Code', 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.waybar-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
padding: 0 20px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.path-navigation {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.path-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 0.9rem;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.path-item:hover {
|
||||
background: rgba(51, 255, 0, 0.1);
|
||||
color: var(--primary-green);
|
||||
border-color: var(--primary-green);
|
||||
}
|
||||
|
||||
/*.path-item.active {
|
||||
background: var(--primary-green);
|
||||
color: var(--bg-dark);
|
||||
font-weight: bold;
|
||||
}*/
|
||||
|
||||
.path-icon {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.waybar-center {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
font-weight: bold;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.logo-brackets {
|
||||
color: var(--secondary-pink);
|
||||
}
|
||||
|
||||
.logo-number {
|
||||
color: var(--primary-green);
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
color: var(--text-secondary);
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.waybar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.language-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
background: rgba(51, 255, 0, 0.1);
|
||||
border: 1px solid var(--primary-green);
|
||||
border-radius: 6px;
|
||||
color: var(--primary-green);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.language-btn:hover {
|
||||
background: var(--primary-green);
|
||||
color: var(--bg-dark);
|
||||
}
|
||||
|
||||
.time-display {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.9rem;
|
||||
padding: 4px 8px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--terminal-border);
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.waybar-container {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.path-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.path-item {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.language-code {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.waybar-header {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.path-item {
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.waybar-right {
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
85
src/lib/components/RetroButton.svelte
Normal file
85
src/lib/components/RetroButton.svelte
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
<script>
|
||||
export let href = '';
|
||||
export let type = 'button';
|
||||
export let variant = 'primary';
|
||||
export let disabled = false;
|
||||
</script>
|
||||
|
||||
{#if href}
|
||||
<a href={href} class="retro-button {variant}" class:disabled>
|
||||
<span class="button-text"><slot /></span>
|
||||
<span class="button-border"></span>
|
||||
</a>
|
||||
{:else}
|
||||
<button type={type} class="retro-button {variant}" class:disabled {disabled}>
|
||||
<span class="button-text"><slot /></span>
|
||||
<span class="button-border"></span>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.retro-button {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding: 12px 24px;
|
||||
background: #000;
|
||||
color: #33ff00;
|
||||
border: none;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 16px;
|
||||
text-transform: uppercase;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.button-text {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.button-border {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
right: -4px;
|
||||
bottom: -4px;
|
||||
border: 2px solid #33ff00;
|
||||
z-index: 1;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.retro-button:hover .button-border {
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
right: 2px;
|
||||
bottom: 2px;
|
||||
}
|
||||
|
||||
.retro-button:active .button-border {
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.retro-button.secondary {
|
||||
color: #ff00ff;
|
||||
}
|
||||
|
||||
.retro-button.secondary .button-border {
|
||||
border-color: #ff00ff;
|
||||
}
|
||||
|
||||
.retro-button.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.retro-button.disabled:hover .button-border {
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
right: -4px;
|
||||
bottom: -4px;
|
||||
}
|
||||
</style>
|
||||
84
src/lib/components/Sections/BlogSection.svelte
Normal file
84
src/lib/components/Sections/BlogSection.svelte
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
<script>
|
||||
import BlogCard from '$lib/components/BlogCard.svelte';
|
||||
import GlitchText from '$lib/components/GlitchText.svelte';
|
||||
import CRTScreen from '$lib/components/CRTScreen.svelte';
|
||||
|
||||
export let blogPosts = [];
|
||||
</script>
|
||||
|
||||
<section class="blog-section">
|
||||
<div class="section-title">
|
||||
<GlitchText text="ПОСЛЕДНИЕ СТАТЬИ" size="2rem" />
|
||||
</div>
|
||||
|
||||
<div class="blog-container">
|
||||
<CRTScreen class="blog-terminal">
|
||||
<div class="terminal-header">
|
||||
<span class="blink">█</span> LATEST BLOG POSTS
|
||||
</div>
|
||||
|
||||
<div class="posts-grid">
|
||||
{#each blogPosts as post}
|
||||
<BlogCard {post} />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="terminal-footer">
|
||||
<a href="/blog" class="view-all">Просмотреть все статьи →</a>
|
||||
</div>
|
||||
</CRTScreen>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.blog-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.blog-container {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
height: 70vh;
|
||||
}
|
||||
|
||||
.blog-terminal {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.posts-grid {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.terminal-footer {
|
||||
padding: 20px;
|
||||
border-top: 1px solid #333;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.view-all {
|
||||
color: #ff00ff;
|
||||
text-decoration: none;
|
||||
font-size: 1.1rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.view-all:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
557
src/lib/components/Sections/HeroSection.svelte
Normal file
557
src/lib/components/Sections/HeroSection.svelte
Normal file
|
|
@ -0,0 +1,557 @@
|
|||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import { Container, Row, Col } from '@sveltestrap/sveltestrap';
|
||||
import GlitchText from '$lib/components/GlitchText.svelte';
|
||||
import CRTScreen from '$lib/components/CRTScreen.svelte';
|
||||
|
||||
const slogans = [
|
||||
"CODE LIKE IT'S 1999",
|
||||
"RETRO FUTURE DEVELOPMENT",
|
||||
"GLITCH IS A FEATURE",
|
||||
"VHS MEETS AI",
|
||||
"OLD SCHOOL COOL",
|
||||
"BYTE THE DUST"
|
||||
];
|
||||
|
||||
const currentSlogan = writable(slogans[0]);
|
||||
const logLines = writable([]);
|
||||
const systemStats = writable({});
|
||||
const fileSystemLines = writable([]);
|
||||
|
||||
// ASCII логотип команды
|
||||
const asciiLogo = `
|
||||
███████╗██████╗ ████████╗███████╗ █████╗ ███╗ ███╗
|
||||
██╔════╝██╔══██╗ ╚══██╔══╝██╔════╝██╔══██╗████╗ ████║
|
||||
█████╗ ██████╔╝ ██║ █████╗ ███████║██╔████╔██║
|
||||
██╔══╝ ██╔══██╗ ██║ ██╔══╝ ██╔══██║██║╚██╔╝██║
|
||||
██║ ██║ ██║ ██║ ███████╗██║ ██║██║ ╚═╝ ██║
|
||||
╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝
|
||||
`.trim();
|
||||
|
||||
let sloganInterval;
|
||||
let logInterval;
|
||||
let statsInterval;
|
||||
let fileSystemInterval;
|
||||
|
||||
onMount(() => {
|
||||
// Смена слоганов
|
||||
let sloganIndex = 0;
|
||||
sloganInterval = setInterval(() => {
|
||||
sloganIndex = (sloganIndex + 1) % slogans.length;
|
||||
currentSlogan.set(slogans[sloganIndex]);
|
||||
}, 3000);
|
||||
|
||||
// Генерация системных логов
|
||||
const logMessages = [
|
||||
"kernel: Initializing retro computing module...",
|
||||
"systemd: Started VHS Effect Service",
|
||||
"network: eth0: link up (1000 Mbps)",
|
||||
"cpu: Frequency scaled to 3.8 GHz",
|
||||
"memory: Allocating 2GB for glitch buffer",
|
||||
"storage: SSD read: 3500 MB/s",
|
||||
"gpu: Rendering 90s effects [OK]",
|
||||
"audio: PCM 44100Hz stereo initialized"
|
||||
];
|
||||
|
||||
logInterval = setInterval(() => {
|
||||
const randomLog = logMessages[Math.floor(Math.random() * logMessages.length)];
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
logLines.update(lines => {
|
||||
const newLines = [...lines, `[${timestamp}] ${randomLog}`];
|
||||
return newLines.slice(-24);
|
||||
});
|
||||
}, 1200);
|
||||
|
||||
// Обновление системной статистики
|
||||
statsInterval = setInterval(() => {
|
||||
systemStats.set({
|
||||
cpu: Math.floor(Math.random() * 30 + 10),
|
||||
memory: Math.floor(Math.random() * 40 + 30),
|
||||
disk: Math.floor(Math.random() * 20 + 10),
|
||||
network: Math.floor(Math.random() * 100 + 50),
|
||||
processes: Math.floor(Math.random() * 50 + 200),
|
||||
uptime: "12d 4h 32m",
|
||||
load: `${(Math.random() * 0.5 + 0.1).toFixed(2)}, ${(Math.random() * 0.4 + 0.08).toFixed(2)}, ${(Math.random() * 0.3 + 0.05).toFixed(2)}`
|
||||
});
|
||||
}, 5000);
|
||||
|
||||
// Генерация файловой системы
|
||||
const fileSystemMessages = [
|
||||
"/home/58team/projects/retro-blog/src/main.svelte [98%]",
|
||||
"/var/log/system/performance.log [2.3MB]",
|
||||
"/etc/config/retro-theme.conf [LOADED]",
|
||||
"/usr/bin/glitch-renderer [RUNNING]",
|
||||
"/tmp/cache/vhs-effects.bin [CACHED]",
|
||||
"/mnt/data/blog-posts/2024/ [12 ITEMS]"
|
||||
];
|
||||
|
||||
fileSystemInterval = setInterval(() => {
|
||||
const randomFile = fileSystemMessages[Math.floor(Math.random() * fileSystemMessages.length)];
|
||||
fileSystemLines.update(lines => {
|
||||
const newLines = [...lines, randomFile];
|
||||
return newLines.slice(-6);
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
clearInterval(sloganInterval);
|
||||
clearInterval(logInterval);
|
||||
clearInterval(statsInterval);
|
||||
clearInterval(fileSystemInterval);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<section class="hero-section">
|
||||
<Container fluid class="h-100">
|
||||
<Row class="h-100 g-3">
|
||||
<!-- Левый столбец - Блоки 1 и 3 под друг другом -->
|
||||
<Col lg={4} class="h-100">
|
||||
<Row class="h-100 g-3">
|
||||
<!-- Блок 1: Логи (верхний) -->
|
||||
<Col class="h-50">
|
||||
<div class="terminal-wrapper h-100">
|
||||
<CRTScreen class="logs-terminal h-100">
|
||||
<div class="terminal-header">
|
||||
<span class="blink">█</span> SYSTEM LOGS
|
||||
</div>
|
||||
<div class="terminal-body">
|
||||
<div class="logs-stream">
|
||||
{#each $logLines as line}
|
||||
<div class="log-line">
|
||||
<span class="log-time">[{new Date().toLocaleTimeString()}]</span>
|
||||
<span class="log-message">{line}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</CRTScreen>
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
<!-- Блок 3: Файловая система (нижний) -->
|
||||
<Col class="h-50">
|
||||
<div class="terminal-wrapper h-100">
|
||||
<CRTScreen class="filesystem-terminal h-100">
|
||||
<div class="terminal-header">
|
||||
<span class="blink">█</span> FILE SYSTEM
|
||||
</div>
|
||||
<div class="terminal-body">
|
||||
<div class="filesystem-stream">
|
||||
{#each $fileSystemLines as line}
|
||||
<div class="file-line">
|
||||
<span class="file-icon">📁</span>
|
||||
<span class="file-path">{line}</span>
|
||||
<span class="file-status">[ACTIVE]</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</CRTScreen>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
|
||||
<!-- Правый столбец - Основной терминал -->
|
||||
<Col lg={8} class="h-100">
|
||||
<div class="terminal-wrapper h-100">
|
||||
<CRTScreen class="main-terminal h-100">
|
||||
<div class="terminal-header">
|
||||
<span class="blink">█</span> [58] TEAM - SYSTEM STATUS
|
||||
</div>
|
||||
|
||||
<div class="terminal-body">
|
||||
<!-- ASCII логотип -->
|
||||
<div class="ascii-art text-center mb-3">
|
||||
<pre class="ascii-logo">{asciiLogo}</pre>
|
||||
</div>
|
||||
|
||||
<!-- Слоган -->
|
||||
<div class="slogan-container mb-3">
|
||||
<div class="command-line">
|
||||
<span class="user">team@58server</span>
|
||||
<span class="path">:~$</span>
|
||||
<span class="command">echo "{@html $currentSlogan.replace(/ /g, ' ')}"</span>
|
||||
</div>
|
||||
<div class="output-line">
|
||||
<span class="output">{$currentSlogan}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Системная информация -->
|
||||
<div class="system-monitor mb-3">
|
||||
<div class="monitor-header d-flex justify-content-between">
|
||||
<span class="title">SYSTEM RESOURCES</span>
|
||||
<span class="refresh">REFRESH: 5s</span>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid">
|
||||
<!-- CPU -->
|
||||
<div class="stat-row d-flex align-items-center gap-2">
|
||||
<span class="stat-label">CPU:</span>
|
||||
<div class="progress flex-grow-1">
|
||||
<div class="progress-bar bg-success" style="width: {$systemStats.cpu}%"></div>
|
||||
</div>
|
||||
<span class="stat-value">{$systemStats.cpu}%</span>
|
||||
</div>
|
||||
|
||||
<!-- Memory -->
|
||||
<div class="stat-row d-flex align-items-center gap-2">
|
||||
<span class="stat-label">MEM:</span>
|
||||
<div class="progress flex-grow-1">
|
||||
<div class="progress-bar bg-info" style="width: {$systemStats.memory}%"></div>
|
||||
</div>
|
||||
<span class="stat-value">{$systemStats.memory}%</span>
|
||||
</div>
|
||||
|
||||
<!-- Disk -->
|
||||
<div class="stat-row d-flex align-items-center gap-2">
|
||||
<span class="stat-label">DISK:</span>
|
||||
<div class="progress flex-grow-1">
|
||||
<div class="progress-bar bg-warning" style="width: {$systemStats.disk}%"></div>
|
||||
</div>
|
||||
<span class="stat-value">{$systemStats.disk}%</span>
|
||||
</div>
|
||||
|
||||
<!-- Network -->
|
||||
<div class="stat-row d-flex align-items-center gap-2">
|
||||
<span class="stat-label">NET:</span>
|
||||
<span class="stat-value">{$systemStats.network} Mbps</span>
|
||||
</div>
|
||||
|
||||
<!-- Processes -->
|
||||
<div class="stat-row d-flex align-items-center gap-2">
|
||||
<span class="stat-label">PROC:</span>
|
||||
<span class="stat-value">{$systemStats.processes}</span>
|
||||
</div>
|
||||
|
||||
<!-- Uptime -->
|
||||
<div class="stat-row d-flex align-items-center gap-2">
|
||||
<span class="stat-label">UPTIME:</span>
|
||||
<span class="stat-value">{$systemStats.uptime}</span>
|
||||
</div>
|
||||
|
||||
<!-- Load -->
|
||||
<div class="stat-row d-flex align-items-center gap-2">
|
||||
<span class="stat-label">LOAD:</span>
|
||||
<span class="stat-value">{$systemStats.load}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Статус сервисов -->
|
||||
<div class="services-status">
|
||||
<div class="service-item d-flex justify-content-between">
|
||||
<span class="service-name">Web Server</span>
|
||||
<span class="service-status running">● RUNNING</span>
|
||||
</div>
|
||||
<div class="service-item d-flex justify-content-between">
|
||||
<span class="service-name">Database</span>
|
||||
<span class="service-status running">● RUNNING</span>
|
||||
</div>
|
||||
<div class="service-item d-flex justify-content-between">
|
||||
<span class="service-name">Cache</span>
|
||||
<span class="service-status running">● RUNNING</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CRTScreen>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.hero-section {
|
||||
height: 100vh;
|
||||
background: var(--bg-dark);
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.terminal-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Фиксированные размеры терминалов */
|
||||
.logs-terminal,
|
||||
.main-terminal,
|
||||
.filesystem-terminal {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.terminal-header {
|
||||
background: var(--primary-green);
|
||||
color: var(--bg-dark);
|
||||
padding: 12px;
|
||||
font-weight: bold;
|
||||
border-bottom: 2px solid var(--bg-dark);
|
||||
font-size: 0.9rem;
|
||||
font-family: 'Courier New', monospace;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.terminal-body {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
overflow: auto;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Стили для основного терминала */
|
||||
.ascii-logo pre {
|
||||
color: var(--primary-green);
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.slogan-container {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid var(--terminal-border);
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.command-line {
|
||||
color: var(--secondary-pink);
|
||||
font-family: 'Courier New', monospace;
|
||||
margin-bottom: 5px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.user {
|
||||
color: var(--accent-blue);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.path {
|
||||
color: var(--primary-green);
|
||||
}
|
||||
|
||||
.command {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.output-line {
|
||||
color: var(--primary-green);
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.system-monitor {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid var(--terminal-border);
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.monitor-header {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.stat-row {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: var(--text-muted);
|
||||
min-width: 50px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
color: var(--primary-green);
|
||||
font-weight: bold;
|
||||
min-width: 60px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Стили для логов */
|
||||
.logs-stream {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.log-line {
|
||||
color: var(--primary-green);
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.8rem;
|
||||
margin-bottom: 6px;
|
||||
opacity: 0;
|
||||
animation: logAppear 0.5s ease forwards, logScroll 12s linear forwards;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
color: var(--text-muted);
|
||||
margin-right: 8px;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
/* Стили для файловой системы */
|
||||
.filesystem-stream {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.file-line {
|
||||
color: var(--primary-green);
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.8rem;
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
opacity: 0;
|
||||
animation: logAppear 0.5s ease forwards, logScroll 10s linear forwards;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.file-path {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-status {
|
||||
color: var(--secondary-pink);
|
||||
font-size: 0.7rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Статус сервисов */
|
||||
.services-status {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid var(--terminal-border);
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.service-item {
|
||||
margin-bottom: 6px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.service-name {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.service-status {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.service-status.running {
|
||||
color: var(--primary-green);
|
||||
}
|
||||
|
||||
/* Анимации */
|
||||
@keyframes logAppear {
|
||||
to {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes logScroll {
|
||||
from {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
to {
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 992px) {
|
||||
.hero-section {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.terminal-body {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.ascii-logo pre {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.hero-section {
|
||||
height: auto;
|
||||
min-height: 100vh;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* На мобильных - вертикальное расположение */
|
||||
.h-50 {
|
||||
height: 300px !important;
|
||||
}
|
||||
|
||||
.terminal-wrapper {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.ascii-logo pre {
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.terminal-header {
|
||||
padding: 10px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.terminal-body {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.ascii-logo pre {
|
||||
font-size: 6px;
|
||||
}
|
||||
|
||||
.slogan-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.system-monitor {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.stat-row {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
min-width: 50px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
161
src/lib/components/Sections/ProjectsSection.svelte
Normal file
161
src/lib/components/Sections/ProjectsSection.svelte
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
<script>
|
||||
import GlitchText from '$lib/components/GlitchText.svelte';
|
||||
import CRTScreen from '$lib/components/CRTScreen.svelte';
|
||||
|
||||
export let projects = [];
|
||||
</script>
|
||||
|
||||
<section class="projects-section">
|
||||
<div class="section-title">
|
||||
<GlitchText text="НАШИ ПРОЕКТЫ" size="2rem" />
|
||||
</div>
|
||||
|
||||
<div class="projects-container">
|
||||
<CRTScreen class="projects-terminal">
|
||||
<div class="terminal-header">
|
||||
<span class="blink">█</span> ACTIVE PROJECTS
|
||||
</div>
|
||||
|
||||
<div class="projects-grid">
|
||||
{#each projects as project}
|
||||
<div class="project-card">
|
||||
<div class="project-header">
|
||||
<h3 class="project-name">{project.name}</h3>
|
||||
<span class:project-status={project.status.toLowerCase()}>
|
||||
{project.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p class="project-description">{project.description}</p>
|
||||
|
||||
<div class="project-tech">
|
||||
{#each project.tech as tech}
|
||||
<span class="tech-tag">{tech}</span>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="project-meta">
|
||||
<span class="project-year">{project.year}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</CRTScreen>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.projects-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
background: #111;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.projects-container {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
height: 70vh;
|
||||
}
|
||||
|
||||
.projects-terminal {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.projects-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
height: calc(100% - 60px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.project-card {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border: 1px solid #33ff00;
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.project-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 0 20px rgba(51, 255, 0, 0.3);
|
||||
}
|
||||
|
||||
.project-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.project-name {
|
||||
color: #33ff00;
|
||||
font-size: 1.2rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.project-status {
|
||||
padding: 4px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.project-status.completed {
|
||||
background: rgba(51, 255, 0, 0.2);
|
||||
color: #33ff00;
|
||||
border: 1px solid #33ff00;
|
||||
}
|
||||
|
||||
.project-status.in-progress {
|
||||
background: rgba(255, 255, 0, 0.2);
|
||||
color: #ffff00;
|
||||
border: 1px solid #ffff00;
|
||||
}
|
||||
|
||||
.project-status.planning {
|
||||
background: rgba(255, 0, 255, 0.2);
|
||||
color: #ff00ff;
|
||||
border: 1px solid #ff00ff;
|
||||
}
|
||||
|
||||
.project-description {
|
||||
color: #ccc;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.project-tech {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.tech-tag {
|
||||
background: rgba(255, 0, 255, 0.2);
|
||||
color: #ff00ff;
|
||||
padding: 3px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.8rem;
|
||||
border: 1px solid #ff00ff;
|
||||
}
|
||||
|
||||
.project-meta {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.project-year {
|
||||
color: #888;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
</style>
|
||||
622
src/lib/components/Sections/TeamSection.svelte
Normal file
622
src/lib/components/Sections/TeamSection.svelte
Normal file
|
|
@ -0,0 +1,622 @@
|
|||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import { Container, Row, Col } from '@sveltestrap/sveltestrap';
|
||||
import GlitchText from '$lib/components/GlitchText.svelte';
|
||||
import CRTScreen from '$lib/components/CRTScreen.svelte';
|
||||
|
||||
const teamStructure = {
|
||||
name: "[58] TEAM",
|
||||
type: "directory",
|
||||
children: [
|
||||
{
|
||||
name: "development",
|
||||
type: "directory",
|
||||
children: [
|
||||
{
|
||||
name: "frontend",
|
||||
type: "directory",
|
||||
children: [
|
||||
{
|
||||
name: "alexey.md",
|
||||
type: "file",
|
||||
member: {
|
||||
id: "1",
|
||||
name: "Алексей Петров",
|
||||
role: "Lead Frontend Developer",
|
||||
avatar: "",
|
||||
bio: "Специалист по Svelte и веб-разработке. Любит чистый код и кофе.",
|
||||
skills: ["Svelte", "JavaScript", "Vue", "React"],
|
||||
articles: [
|
||||
{ title: "Svelte для начинающих", date: "2024-01-15", views: 1243 },
|
||||
{ title: "Оптимизация производительности", date: "2024-01-10", views: 876 },
|
||||
{ title: "TypeScript лучшие практики", date: "2024-01-05", views: 954 }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "maria.md",
|
||||
type: "file",
|
||||
member: {
|
||||
id: "2",
|
||||
name: "Мария Сидорова",
|
||||
role: "UI/UX Developer",
|
||||
avatar: "",
|
||||
bio: "Создает интерфейсы которые нравятся пользователям. Верит в силу дизайна.",
|
||||
skills: ["Figma", "CSS", "Sass", "UI Design"],
|
||||
articles: [
|
||||
{ title: "Дизайн системы в 2024", date: "2024-01-14", views: 1102 },
|
||||
{ title: "Анимации в вебе", date: "2024-01-09", views: 723 },
|
||||
{ title: "Цветовые палитры", date: "2024-01-04", views: 645 }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "backend",
|
||||
type: "directory",
|
||||
children: [
|
||||
{
|
||||
name: "ivan.md",
|
||||
type: "file",
|
||||
member: {
|
||||
id: "3",
|
||||
name: "Иван Козлов",
|
||||
role: "Backend Lead",
|
||||
avatar: "",
|
||||
bio: "Работает с базами данных и API. Любит сложные задачи.",
|
||||
skills: ["Node.js", "Python", "PostgreSQL", "Docker"],
|
||||
articles: [
|
||||
{ title: "REST API лучшие практики", date: "2024-01-13", views: 987 },
|
||||
{ title: "Базы данных для начинающих", date: "2024-01-08", views: 834 },
|
||||
{ title: "Микросервисная архитектура", date: "2024-01-03", views: 1123 }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "design",
|
||||
type: "directory",
|
||||
children: [
|
||||
{
|
||||
name: "anna.md",
|
||||
type: "file",
|
||||
member: {
|
||||
id: "4",
|
||||
name: "Анна Волкова",
|
||||
role: "Creative Director",
|
||||
avatar: "",
|
||||
bio: "Pixel perfectionist с любовью к ретро стилю.",
|
||||
skills: ["Figma", "Photoshop", "Illustrator", "Blender"],
|
||||
articles: [
|
||||
{ title: "Ретро дизайн в 2024", date: "2024-01-12", views: 1456 },
|
||||
{ title: "3D в веб дизайне", date: "2024-01-07", views: 678 },
|
||||
{ title: "Шрифты и типографика", date: "2024-01-02", views: 789 }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const selectedMember = writable(null);
|
||||
const expandedFolders = writable(new Set(['development', 'design']));
|
||||
const isMobile = writable(false);
|
||||
|
||||
onMount(() => {
|
||||
isMobile.set(window.innerWidth < 768);
|
||||
const handleResize = () => isMobile.set(window.innerWidth < 768);
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
function toggleFolder(folderName) {
|
||||
expandedFolders.update(set => {
|
||||
const newSet = new Set(set);
|
||||
newSet.has(folderName) ? newSet.delete(folderName) : newSet.add(folderName);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
|
||||
function selectMember(member) {
|
||||
selectedMember.set(member);
|
||||
}
|
||||
|
||||
function isExpanded(folderName) {
|
||||
return $expandedFolders.has(folderName);
|
||||
}
|
||||
|
||||
function formatDate(dateString) {
|
||||
return new Date(dateString).toLocaleDateString('ru-RU');
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="team-section">
|
||||
<div class="section-title">
|
||||
<GlitchText text="TEAM DIRECTORY" size="2rem" />
|
||||
</div>
|
||||
|
||||
<Container fluid>
|
||||
<Row class="g-3">
|
||||
<Col lg={4} class="terminal-col">
|
||||
<div class="terminal-wrapper h-100">
|
||||
<CRTScreen class="directory-terminal h-100">
|
||||
<div class="terminal-header">
|
||||
<span class="blink">█</span> TEAM STRUCTURE
|
||||
</div>
|
||||
|
||||
<div class="terminal-body">
|
||||
<div class="directory-tree">
|
||||
<div class="tree-item directory root" on:click={() => toggleFolder('root')}>
|
||||
<span class="folder-icon">{isExpanded('root') ? '📂' : '📁'}</span>
|
||||
<span class="item-name">[58] TEAM/</span>
|
||||
</div>
|
||||
|
||||
{#each teamStructure.children as department}
|
||||
<div class="tree-branch">
|
||||
<div class="tree-item directory" on:click={() => toggleFolder(department.name)}>
|
||||
<span class="folder-icon">{isExpanded(department.name) ? '📂' : '📁'}</span>
|
||||
<span class="item-name">{department.name}/</span>
|
||||
</div>
|
||||
|
||||
{#if isExpanded(department.name)}
|
||||
{#each department.children as team}
|
||||
<div class="tree-sub-branch">
|
||||
<div class="tree-item directory" on:click={() => toggleFolder(team.name)}>
|
||||
<span class="folder-icon">{isExpanded(team.name) ? '📂' : '📁'}</span>
|
||||
<span class="item-name">{team.name}/</span>
|
||||
</div>
|
||||
|
||||
{#if isExpanded(team.name)}
|
||||
{#each team.children as member}
|
||||
<div class="tree-item file" on:click={() => selectMember(member.member)}>
|
||||
<span class="file-icon">📄</span>
|
||||
<span class="item-name">{member.name}</span>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="terminal-footer">
|
||||
<div class="footer-stats">
|
||||
<span class="stat">Members: 4</span>
|
||||
<span class="stat">Articles: 12</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CRTScreen>
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
<Col lg={8} class="terminal-col">
|
||||
<div class="terminal-wrapper h-100">
|
||||
<CRTScreen class="member-terminal h-100">
|
||||
<div class="terminal-header">
|
||||
<span class="blink">█</span>
|
||||
{#if $selectedMember}
|
||||
MEMBER PROFILE: {$selectedMember.name}
|
||||
{:else}
|
||||
SELECT TEAM MEMBER
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="terminal-body">
|
||||
{#if $selectedMember}
|
||||
<div class="member-header">
|
||||
<h3 class="member-name">{$selectedMember.name}</h3>
|
||||
<p class="member-role">{$selectedMember.role}</p>
|
||||
<p class="member-bio">{$selectedMember.bio}</p>
|
||||
</div>
|
||||
|
||||
<div class="member-skills">
|
||||
<h4 class="section-title">SKILLS:</h4>
|
||||
<div class="skills-grid">
|
||||
{#each $selectedMember.skills as skill}
|
||||
<span class="skill-tag">{skill}</span>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="member-articles">
|
||||
<h4 class="section-title">LATEST ARTICLES:</h4>
|
||||
<div class="articles-list">
|
||||
{#each $selectedMember.articles as article, index}
|
||||
<div class="article-item">
|
||||
<div class="article-header">
|
||||
<span class="article-number">[{index + 1}]</span>
|
||||
<span class="article-title">{article.title}</span>
|
||||
</div>
|
||||
<div class="article-meta">
|
||||
<span class="article-date">{formatDate(article.date)}</span>
|
||||
<span class="article-views">👁️{article.views}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="member-stats">
|
||||
<h4 class="section-title">STATISTICS:</h4>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Articles:</span>
|
||||
<span class="stat-value">{$selectedMember.articles.length}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Total Views:</span>
|
||||
<span class="stat-value">
|
||||
{$selectedMember.articles.reduce((sum, article) => sum + article.views, 0).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Avg. Views:</span>
|
||||
<span class="stat-value">
|
||||
{Math.round($selectedMember.articles.reduce((sum, article) => sum + article.views, 0) / $selectedMember.articles.length).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{:else}
|
||||
<div class="no-selection">
|
||||
<div class="terminal-message">
|
||||
<pre class="ascii-art">
|
||||
╔══════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║ Select a team member from the directory ║
|
||||
║ to view their profile and latest articles ║
|
||||
║ ║
|
||||
║ Use the file tree on the left to navigate ║
|
||||
║ through our team structure ║
|
||||
║ ║
|
||||
╚══════════════════════════════════════════════╝
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</CRTScreen>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.team-section {
|
||||
min-height: 100vh;
|
||||
padding: 40px 20px;
|
||||
background: var(--bg-darker);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.terminal-col {
|
||||
height: 70vh;
|
||||
}
|
||||
|
||||
.terminal-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.directory-terminal, .member-terminal {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.terminal-header {
|
||||
background: var(--primary-green);
|
||||
color: var(--bg-dark);
|
||||
padding: 12px;
|
||||
font-weight: bold;
|
||||
border-bottom: 2px solid var(--bg-dark);
|
||||
font-size: 0.9rem;
|
||||
font-family: 'Courier New', monospace;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.terminal-body {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
overflow: auto;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Дерево директорий */
|
||||
.directory-tree {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.tree-branch {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.tree-sub-branch {
|
||||
margin-left: 25px;
|
||||
}
|
||||
|
||||
.tree-item {
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
border-radius: 3px;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tree-item:hover {
|
||||
background: rgba(51, 255, 0, 0.1);
|
||||
}
|
||||
|
||||
.tree-item.directory {
|
||||
color: var(--primary-green);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tree-item.file {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.tree-item.root {
|
||||
color: var(--secondary-pink);
|
||||
font-weight: bold;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.folder-icon, .file-icon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.terminal-footer {
|
||||
margin-top: 20px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid var(--terminal-border);
|
||||
}
|
||||
|
||||
.footer-stats {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Информация о сотруднике */
|
||||
.member-header {
|
||||
margin-bottom: 25px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid var(--terminal-border);
|
||||
}
|
||||
|
||||
.member-name {
|
||||
color: var(--primary-green);
|
||||
font-size: 1.8rem;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.member-role {
|
||||
color: var(--secondary-pink);
|
||||
font-size: 1.1rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.member-bio {
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.6;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
color: var(--primary-green);
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 15px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.skills-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.skill-tag {
|
||||
background: rgba(51, 255, 0, 0.2);
|
||||
color: var(--primary-green);
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.85rem;
|
||||
border: 1px solid var(--terminal-border);
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.articles-list {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.article-item {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid var(--terminal-border);
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
margin-bottom: 10px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.article-item:hover {
|
||||
background: rgba(51, 255, 0, 0.1);
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.article-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.article-number {
|
||||
color: var(--secondary-pink);
|
||||
font-weight: bold;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.article-title {
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.article-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid var(--terminal-border);
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.8rem;
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
color: var(--primary-green);
|
||||
font-weight: bold;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* Сообщение при отсутствии выбора */
|
||||
.no-selection {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.terminal-message {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ascii-art pre {
|
||||
color: var(--primary-green);
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 10px;
|
||||
line-height: 1.2;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 992px) {
|
||||
.terminal-col {
|
||||
height: 50vh;
|
||||
}
|
||||
|
||||
.member-name {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.team-section {
|
||||
padding: 20px 15px;
|
||||
}
|
||||
|
||||
.terminal-col {
|
||||
height: 400px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.terminal-body {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.tree-branch {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.tree-sub-branch {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.member-name {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.skills-grid {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.skill-tag {
|
||||
padding: 4px 8px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.ascii-art pre {
|
||||
font-size: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.terminal-col {
|
||||
height: 350px;
|
||||
}
|
||||
|
||||
.terminal-body {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.member-name {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.article-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.ascii-art pre {
|
||||
font-size: 6px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
95
src/lib/components/TeamCard.svelte
Normal file
95
src/lib/components/TeamCard.svelte
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
<script>
|
||||
import GlitchText from './GlitchText.svelte';
|
||||
import CRTScreen from './CRTScreen.svelte';
|
||||
|
||||
export let member = {
|
||||
id: '',
|
||||
name: '',
|
||||
role: '',
|
||||
avatar: '',
|
||||
bio: ''
|
||||
};
|
||||
</script>
|
||||
|
||||
<CRTScreen class="team-card">
|
||||
<div class="card-content">
|
||||
<div class="avatar">
|
||||
{#if member.avatar}
|
||||
<img src={member.avatar} alt={member.name} />
|
||||
{:else}
|
||||
<div class="avatar-placeholder">👤</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<h3 class="member-name">
|
||||
<GlitchText text={member.name} size="1.1rem" />
|
||||
</h3>
|
||||
|
||||
<p class="member-role">{member.role}</p>
|
||||
|
||||
<p class="member-bio">{member.bio}</p>
|
||||
|
||||
<div class="card-actions">
|
||||
<a href={`/team/${member.id}`} class="view-profile">Профиль →</a>
|
||||
</div>
|
||||
</div>
|
||||
</CRTScreen>
|
||||
|
||||
<style>
|
||||
.team-card {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
margin: 0 auto 15px;
|
||||
overflow: hidden;
|
||||
border: 2px solid #33ff00;
|
||||
}
|
||||
|
||||
.avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 2rem;
|
||||
background: #222;
|
||||
}
|
||||
|
||||
.member-name {
|
||||
margin: 0 0 5px 0;
|
||||
color: #33ff00;
|
||||
}
|
||||
|
||||
.member-role {
|
||||
color: #ff00ff;
|
||||
margin: 0 0 10px 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.member-bio {
|
||||
color: #ccc;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.view-profile {
|
||||
color: #ff00ff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.view-profile:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
43
src/lib/config.js
Normal file
43
src/lib/config.js
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
export const siteConfig = {
|
||||
title: 'DevTeam Blog',
|
||||
description: 'Блог команды разработчиков о технологиях и разработке',
|
||||
baseUrl: import.meta.env.VITE_BASE_URL || 'http://localhost:5173',
|
||||
apiUrl: import.meta.env.VITE_API_URL || 'http://localhost:3000/api',
|
||||
|
||||
team: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Иван Иванов',
|
||||
position: 'Senior Frontend Developer'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Петр Петров',
|
||||
position: 'Fullstack Developer'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Мария Сидорova',
|
||||
position: 'UI/UX Designer'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Алексей Смирнов',
|
||||
position: 'DevOps Engineer'
|
||||
}
|
||||
],
|
||||
|
||||
socialLinks: {
|
||||
github: 'https://github.com/devteam',
|
||||
twitter: 'https://twitter.com/devteam',
|
||||
linkedin: 'https://linkedin.com/company/devteam'
|
||||
},
|
||||
|
||||
features: [
|
||||
'Веб-разработка',
|
||||
'Мобильные приложения',
|
||||
'UI/UX дизайн',
|
||||
'DevOps и инфраструктура',
|
||||
'Консалтинг и аудит'
|
||||
]
|
||||
};
|
||||
1
src/lib/index.js
Normal file
1
src/lib/index.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
// Reexport your entry components here
|
||||
20
src/lib/services/api.js
Normal file
20
src/lib/services/api.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import axios from "axios";
|
||||
const API_BASE_URL = 'http://localhost:3000/api';
|
||||
|
||||
const apiClient = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
});
|
||||
|
||||
apiClient.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
console.error('API Error:', error);
|
||||
throw error;
|
||||
}
|
||||
);
|
||||
|
||||
export default apiClient;
|
||||
164
src/lib/services/blogService.js
Normal file
164
src/lib/services/blogService.js
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
import apiClient from './api.js';
|
||||
|
||||
export const blogService = {
|
||||
// Получить все статьи
|
||||
async getBlogPosts(params = {}) {
|
||||
try {
|
||||
const response = await apiClient.get('/posts', { params });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching blog posts:', error);
|
||||
return getMockBlogPosts();
|
||||
}
|
||||
},
|
||||
|
||||
// Получить статью по ID
|
||||
async getPostById(id) {
|
||||
try {
|
||||
const response = await apiClient.get(`/posts/${id}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching post:', error);
|
||||
return getMockBlogPosts().find(post => post.id === id) || null;
|
||||
}
|
||||
},
|
||||
|
||||
// Получить статьи по категории
|
||||
async getPostsByCategory(category, params = {}) {
|
||||
try {
|
||||
const response = await apiClient.get('/posts', {
|
||||
params: { category, ...params }
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching posts by category:', error);
|
||||
return getMockBlogPosts().filter(post => post.category === category);
|
||||
}
|
||||
},
|
||||
|
||||
// Получить статьи по тегу
|
||||
async getPostsByTag(tag, params = {}) {
|
||||
try {
|
||||
const response = await apiClient.get('/posts', {
|
||||
params: { tag, ...params }
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching posts by tag:', error);
|
||||
return getMockBlogPosts().filter(post => post.tags.includes(tag));
|
||||
}
|
||||
},
|
||||
|
||||
// Получить статьи по автору
|
||||
async getPostsByAuthor(username, params = {}) {
|
||||
try {
|
||||
const response = await apiClient.get('/posts', {
|
||||
params: { username, ...params }
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching posts by author:', error);
|
||||
return getMockBlogPosts().filter(post => post.username === username);
|
||||
}
|
||||
},
|
||||
|
||||
// Получить метаданные для фильтров
|
||||
async getBlogMetadata() {
|
||||
try {
|
||||
const posts = await this.getBlogPosts();
|
||||
return generateMetadataFromPosts(posts);
|
||||
} catch (error) {
|
||||
console.error('Error fetching blog metadata:', error);
|
||||
return generateMetadataFromPosts(getMockBlogPosts());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Генерация метаданных из списка постов
|
||||
function generateMetadataFromPosts(posts) {
|
||||
const categories = [...new Set(posts.map(post => post.category))];
|
||||
const tags = [...new Set(posts.flatMap(post => post.tags || []))];
|
||||
const authors = [...new Set(posts.map(post => post.username))];
|
||||
|
||||
return {
|
||||
categories: categories.map(cat => ({
|
||||
name: cat,
|
||||
count: posts.filter(post => post.category === cat).length
|
||||
})),
|
||||
tags: tags.map(tag => ({
|
||||
name: tag,
|
||||
count: posts.filter(post => post.tags && post.tags.includes(tag)).length
|
||||
})),
|
||||
authors: authors.map(author => ({
|
||||
name: author,
|
||||
count: posts.filter(post => post.username === author).length
|
||||
})),
|
||||
totalPosts: posts.length
|
||||
};
|
||||
}
|
||||
|
||||
// Mock данные соответствующие новой структуре API
|
||||
function getMockBlogPosts() {
|
||||
return [
|
||||
{
|
||||
id: "1",
|
||||
title: "Svelte 5: Полное руководство",
|
||||
description: "Изучите все нововведения в Svelte 5, включая runes и улучшенную реактивность",
|
||||
category: "frontend",
|
||||
tags: ["svelte", "javascript", "frontend"],
|
||||
username: "Алексей Петров",
|
||||
content: "adasdasdasdasd",
|
||||
updatedAt: "2024-01-20T10:00:00Z"
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
title: "Ретро дизайн в 2024 году",
|
||||
description: "Как использовать эстетику 90-х в современных веб-проектах",
|
||||
category: "design",
|
||||
tags: ["design", "retro", "ui/ux"],
|
||||
username: "Мария Сидорова",
|
||||
content: "adasdasdasdasd",
|
||||
updatedAt: "2024-01-18T14:30:00Z"
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
title: "Лучшие практики безопасности API",
|
||||
description: "Полное руководство по защите вашего API от уязвимостей",
|
||||
category: "backend",
|
||||
tags: ["security", "api", "backend"],
|
||||
username: "Иван Козлов",
|
||||
content: "adasdasdasdasd",
|
||||
updatedAt: "2024-01-15T09:15:00Z"
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
title: "Оптимизация производительности веб-приложений",
|
||||
description: "Современные техники для ускорения загрузки и отзывчивости",
|
||||
category: "frontend",
|
||||
tags: ["performance", "optimization", "javascript"],
|
||||
username: "Алексей Петров",
|
||||
content: "adasdasdasdasd",
|
||||
updatedAt: "2024-01-12T16:45:00Z"
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
title: "Docker для веб-разработчиков",
|
||||
description: "Практическое руководство по контейнеризации приложений",
|
||||
category: "devops",
|
||||
tags: ["docker", "devops", "containers"],
|
||||
content: "adasdasdasdasd",
|
||||
username: "Иван Козлов",
|
||||
updatedAt: "2024-01-10T11:20:00Z"
|
||||
},
|
||||
{
|
||||
id: "6",
|
||||
title: "CSS Grid vs Flexbox: Когда что использовать",
|
||||
description: "Подробное сравнение двух основных CSS технологий верстки",
|
||||
category: "frontend",
|
||||
tags: ["css", "grid", "flexbox", "design"],
|
||||
username: "Мария Сидорова",
|
||||
content: "adasdasdasdasd",
|
||||
updatedAt: "2024-01-08T13:10:00Z"
|
||||
}
|
||||
];
|
||||
}
|
||||
110
src/lib/services/teamService.js
Normal file
110
src/lib/services/teamService.js
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import apiClient from './api.js';
|
||||
|
||||
export const teamService = {
|
||||
// Получить всех членов команды
|
||||
async getTeamMembers() {
|
||||
try {
|
||||
const response = await apiClient.get('/team');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching team members:', error);
|
||||
// Возвращаем mock данные в случае ошибки
|
||||
return getMockTeamMembers();
|
||||
}
|
||||
},
|
||||
|
||||
// Получить конкретного члена команды
|
||||
async getTeamMember(id) {
|
||||
try {
|
||||
const response = await apiClient.get(`/team/${id}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching team member:', error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
// Получить случайных членов команды для карусели
|
||||
async getRandomMembers(limit = 4) {
|
||||
try {
|
||||
const response = await apiClient.get('/team/random', {
|
||||
params: { limit }
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching random members:', error);
|
||||
return getMockTeamMembers().slice(0, limit);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Mock данные для разработки
|
||||
function getMockTeamMembers() {
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
username: "coma",
|
||||
name: "Coma",
|
||||
role: "DevOps Engineer",
|
||||
specialty: "Системное администрирование & Девопс",
|
||||
description: "Отвечает за инфраструктуру, развертывание и мониторинг. Следит за тем, чтобы все системы работали как часы.",
|
||||
skills: ["Docker", "Kubernetes", "AWS", "CI/CD", "Linux", "Monitoring"],
|
||||
avatar: "⚙️",
|
||||
joinDate: "2023",
|
||||
projects: ["Infrastructure", "Deployment", "Monitoring"],
|
||||
motto: "Если что-то можно автоматизировать - это должно быть автоматизировано"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
username: "muts",
|
||||
name: "Muts",
|
||||
role: "Embedded/Backend Developer",
|
||||
specialty: "Встроенные системы & Бэкенд разработка",
|
||||
description: "Работает на стыке hardware и software. Создает эффективные решения для встроенных систем и серверной части.",
|
||||
skills: ["C/C++", "Python", "Embedded", "Rust", "API Design", "Microcontrollers"],
|
||||
avatar: "🔌",
|
||||
joinDate: "2023",
|
||||
projects: ["Embedded Systems", "Backend API", "Hardware Integration"],
|
||||
motto: "Код должен быть быстрым как железо, и надежным как скала"
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
username: "denzz",
|
||||
name: "Denzz",
|
||||
role: "Frontend Developer",
|
||||
specialty: "Пользовательские интерфейсы & Веб-разработка",
|
||||
description: "Создает интуитивные и красивые интерфейсы. Превращает сложные идеи в простые и элегантные решения.",
|
||||
skills: ["Svelte", "JavaScript", "TypeScript", "CSS", "UI/UX", "Animation"],
|
||||
avatar: "🎨",
|
||||
joinDate: "2023",
|
||||
projects: ["Web Interfaces", "User Experience", "Visual Design"],
|
||||
motto: "Хороший интерфейс - это когда пользователь не замечает интерфейс"
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
username: "itc1205",
|
||||
name: "ITC1205",
|
||||
role: "CEO & Backend Developer",
|
||||
specialty: "Техническое руководство & Архитектура систем",
|
||||
description: "Совмещает техническое видение с бизнес-стратегией. Отвечает за архитектурные решения и развитие команды.",
|
||||
skills: ["System Architecture", "Project Management", "Node.js", "Python", "Leadership", "Strategy"],
|
||||
avatar: "👑",
|
||||
joinDate: "2023",
|
||||
projects: ["Technical Strategy", "System Architecture", "Team Leadership"],
|
||||
motto: "Технологии должны решать реальные проблемы, а не создавать новые"
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
username: "d1nnoise",
|
||||
name: "D1nnoise",
|
||||
role: "QA Engineer",
|
||||
specialty: "Тестирование & Обеспечение качества",
|
||||
description: "Гарантирует качество и надежность продуктов. Находит баги до того, как их найдут пользователи.",
|
||||
skills: ["Testing", "Automation", "Quality Assurance", "Bug Tracking", "Test Planning", "CI/CD"],
|
||||
avatar: "🐛",
|
||||
joinDate: "2023",
|
||||
projects: ["Quality Assurance", "Testing Automation", "Process Improvement"],
|
||||
motto: "Идеального кода не существует, но к нему нужно стремиться"
|
||||
}
|
||||
];
|
||||
}
|
||||
32
src/lib/utils/date.js
Normal file
32
src/lib/utils/date.js
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
export function formatDate(dateString) {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('ru-RU', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
}
|
||||
|
||||
export function formatDateTime(dateString) {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('ru-RU', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
export function getRelativeTime(dateString) {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diffInSeconds = Math.floor((now - date) / 1000);
|
||||
|
||||
if (diffInSeconds < 60) return 'только что';
|
||||
if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)} мин. назад`;
|
||||
if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)} ч. назад`;
|
||||
if (diffInSeconds < 604800) return `${Math.floor(diffInSeconds / 86400)} дн. назад`;
|
||||
|
||||
return formatDate(dateString);
|
||||
}
|
||||
26
src/lib/utils/markdown.js
Normal file
26
src/lib/utils/markdown.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { marked } from 'marked';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
|
||||
marked.setOptions({
|
||||
breaks: true,
|
||||
gfm: true
|
||||
});
|
||||
|
||||
const sanitizeOptions = {
|
||||
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img', 'h1', 'h2', 'h3']),
|
||||
allowedAttributes: {
|
||||
...sanitizeHtml.defaults.allowedAttributes,
|
||||
img: ['src', 'alt', 'title'],
|
||||
a: ['href', 'name', 'target']
|
||||
}
|
||||
};
|
||||
|
||||
export function parseMarkdown(content) {
|
||||
const html = marked.parse(content);
|
||||
return sanitizeHtml(html, sanitizeOptions);
|
||||
}
|
||||
|
||||
export function extractExcerpt(content, length = 150) {
|
||||
const plainText = content.replace(/[#*`~]/g, '').replace(/\n/g, ' ');
|
||||
return plainText.slice(0, length) + (plainText.length > length ? '...' : '');
|
||||
}
|
||||
960
src/routes/(main)/+page.svelte
Normal file
960
src/routes/(main)/+page.svelte
Normal 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>
|
||||
942
src/routes/(main)/about/+page.svelte
Normal file
942
src/routes/(main)/about/+page.svelte
Normal 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>
|
||||
837
src/routes/(main)/blog/+page.svelte
Normal file
837
src/routes/(main)/blog/+page.svelte
Normal 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>
|
||||
|
||||
5
src/routes/(main)/blog/[id]/+page.js
Normal file
5
src/routes/(main)/blog/[id]/+page.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export async function load({ params }) {
|
||||
return {
|
||||
id: params.id
|
||||
};
|
||||
}
|
||||
657
src/routes/(main)/blog/[id]/+page.svelte
Normal file
657
src/routes/(main)/blog/[id]/+page.svelte
Normal 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>
|
||||
827
src/routes/(main)/team/+page.svelte
Normal file
827
src/routes/(main)/team/+page.svelte
Normal 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>
|
||||
76
src/routes/(main)/team/+server.js
Normal file
76
src/routes/(main)/team/+server.js
Normal 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
23
src/routes/+error.svelte
Normal 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
18
src/routes/+layout.svelte
Normal 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>
|
||||
27
src/routes/admin/+page.svelte
Normal file
27
src/routes/admin/+page.svelte
Normal 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;
|
||||
}
|
||||
}
|
||||
};
|
||||
77
src/routes/api/posts/+server.js
Normal file
77
src/routes/api/posts/+server.js
Normal 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 });
|
||||
}
|
||||
}
|
||||
|
||||
57
src/routes/api/team/+server.js
Normal file
57
src/routes/api/team/+server.js
Normal 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
1
static/favicon.svg
Normal 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
14
svelte.config.js
Normal 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
6
vite.config.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()]
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue