diff --git a/backend/src/endpoints/api/spotify/models.rs b/backend/src/endpoints/api/spotify/models.rs index 66c9ff5..731e0e8 100644 --- a/backend/src/endpoints/api/spotify/models.rs +++ b/backend/src/endpoints/api/spotify/models.rs @@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize}; pub struct NowPlayingResponse { pub item: Option, pub is_playing: bool, + pub device: Option, } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -34,6 +35,12 @@ pub struct ExternalUrls { pub spotify: String, } +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Device { + pub volume_percent: Option, +} + + #[derive(Debug, Deserialize)] pub struct TokenResponse { pub access_token: String, @@ -61,4 +68,4 @@ pub struct NowPlayingStreamData { #[serde(rename = "songUrl")] #[serde(skip_serializing_if = "Option::is_none")] pub song_url: Option, -} +} \ No newline at end of file diff --git a/backend/src/endpoints/api/spotify/services.rs b/backend/src/endpoints/api/spotify/services.rs index 3810586..acb672d 100644 --- a/backend/src/endpoints/api/spotify/services.rs +++ b/backend/src/endpoints/api/spotify/services.rs @@ -36,7 +36,8 @@ impl SpotifyService { let client = reqwest::Client::new(); let response = client - .get("https://api.spotify.com/v1/me/player/currently-playing") + // Изменяем эндпоинт, чтобы получить больше данных, включая громкость + .get("https://api.spotify.com/v1/me/player?additional_types=track") .header("Authorization", format!("Bearer {}", access_token)) .send() .await @@ -46,7 +47,22 @@ impl SpotifyService { match status { StatusCode::OK => { - let now_playing_response = response.json::().await?; + // Делаем переменную изменяемой, чтобы модифицировать ее + let mut now_playing_response = response.json::().await?; + + // === НАША НОВАЯ ЛОГИКА === + // Проверяем, есть ли информация об устройстве и громкости + if let Some(device) = &now_playing_response.device { + if let Some(volume) = device.volume_percent { + // Если громкость равна 0, считаем, что музыка не играет + if volume == 0 { + now_playing_response.is_playing = false; + info!("Spotify volume is 0, setting is_playing to false."); + } + } + } + // ======================= + return Ok(Some(now_playing_response)); } StatusCode::NO_CONTENT => { @@ -129,4 +145,4 @@ impl SpotifyService { Err(AppError::InternalError(error_message)) } } -} +} \ No newline at end of file diff --git a/frontend/app/[locale]/about/page.module.css b/frontend/app/[locale]/about/page.module.css index 0a50b1c..80202b3 100644 --- a/frontend/app/[locale]/about/page.module.css +++ b/frontend/app/[locale]/about/page.module.css @@ -21,21 +21,49 @@ border: 1px solid var(--border); background-color: var(--secondary); color: var(--secondary-foreground); - transition: background-color 0.2s; + transition: background-color 0.2s, transform 0.2s; margin-top: 10px; } .contactButton:hover { background-color: var(--accent); + transform: translateY(-2px); } .contactDetails { - margin-top: 15px; + margin-top: 20px; padding: 15px; background-color: var(--muted); border-radius: 8px; + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; } +.contactLink { + display: flex; + align-items: center; + gap: 10px; + color: var(--foreground); + text-decoration: none; + font-size: 1rem; + transition: color 0.2s; +} + +.contactLink:hover { + color: var(--primary); +} + +.contactLink svg { + width: 20px; + height: 20px; +} +.dark .contactLink svg { + color: var(--primary-foreground); +} + + .contentWrapper { display: flex; flex-direction: row-reverse; @@ -68,6 +96,48 @@ text-decoration: underline; } +/* Стили для карточек */ +.card { + background-color: var(--card); + border: 1px solid var(--border); + border-radius: 12px; + padding: 20px; + transition: box-shadow 0.3s ease, transform 0.3s ease; +} + +.card:hover { + transform: translateY(-5px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.07); +} + +.dark .card:hover { + box-shadow: 0 8px 25px rgba(255, 255, 255, 0.05); +} + + +.skillsContainer { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 15px; +} + +.skillTag { + background-color: var(--secondary); + color: var(--secondary-foreground); + padding: 6px 14px; + border-radius: 16px; + font-size: 0.9rem; + font-weight: 500; + border: 1px solid var(--border); + transition: background-color 0.2s, transform 0.2s; +} + +.skillTag:hover { + background-color: var(--accent); + transform: scale(1.05); +} + .statsImages { display: flex; flex-direction: column; @@ -84,28 +154,16 @@ height: auto; } -.connect { - margin-top: 30px; - padding-top: 20px; - border-top: 1px solid var(--border); -} - -.socialLinks { - display: flex; - gap: 15px; - margin-top: 10px; +/* Стили для анимации */ +.animatedSection { + opacity: 0; + transform: translateY(20px); + transition: opacity 0.6s ease-out, transform 0.6s ease-out; } -.socialIcon { - height: 40px; - width: 40px; - object-fit: contain; - filter: grayscale(1); - transition: filter 0.2s; -} - -.socialIcon:hover { - filter: grayscale(0); +.animatedSection.isVisible { + opacity: 1; + transform: translateY(0); } @media (max-width: 768px) { @@ -115,4 +173,4 @@ .sidebar { max-width: 100%; } -} +} \ No newline at end of file diff --git a/frontend/app/[locale]/about/page.tsx b/frontend/app/[locale]/about/page.tsx index e239c0e..144f429 100644 --- a/frontend/app/[locale]/about/page.tsx +++ b/frontend/app/[locale]/about/page.tsx @@ -1,66 +1,123 @@ 'use client'; -import { useState } from 'react'; +import { useState, useEffect, useRef } from 'react'; import Link from 'next/link'; import Image from 'next/image'; +import { useTranslations } from 'next-intl'; import styles from './page.module.css'; +const useIntersectionObserver = (options: IntersectionObserverInit) => { + const [elements, setElements] = useState([]); + const [entries, setEntries] = useState([]); + const observer = useRef(null); + + useEffect(() => { + if (elements.length) { + observer.current = new IntersectionObserver((ioEntries) => { + setEntries(ioEntries); + }, options); + + elements.forEach(element => observer.current?.observe(element)); + } + return () => { + if (observer.current) { + observer.current.disconnect(); + } + }; + }, [elements, options]); + + return [observer.current, setElements, entries] as const; +}; + + export default function ResumePage() { + const t = useTranslations('AboutPage'); const [showContact, setShowContact] = useState(false); + const skillList: string[] = t.raw('skills.list'); + + const [observer, setElements, entries] = useIntersectionObserver({ + threshold: 0.2, + root: null + }); + + useEffect(() => { + const sections = Array.from(document.querySelectorAll(`.${styles.animatedSection}`)); + setElements(sections as HTMLElement[]); + }, [setElements]); + + useEffect(() => { + entries.forEach(entry => { + if (entry.isIntersecting) { + entry.target.classList.add(styles.isVisible); + observer?.unobserve(entry.target); + } + }); + }, [entries, observer]); return (
-
-

Хохлов Дмитрий

-

FullStack Developer (TypeScript, Rust)

+
+

{t('name')}

+

{t('role')}

{showContact && (
-

Телефон: +7 (932) 477-0975

-

Email: hohlov.03@inbox.ru

-

Telegram: @diametrfq

+ + call + {t('contact.phone')} + + + email + {t('contact.email')} + + + {t('contact.telegram')} +
)}
-
-

Обо мне

-

- Я опытный FullStack разработчик с более чем четырехлетним опытом в создании - эффективных веб-приложений. Мои навыки охватывают весь цикл разработки, от концепции и дизайна до реализации и поддержки. -

-

- Активно следую за последними тенденциями веб-разработки, увлечен созданием чистого, эффективного и масштабируемого кода. -

+
+

{t('about.title')}

+

{t('about.paragraph1')}

+

{t('about.paragraph2')}

-
-

Опыт работы

-

Junior FullStack Developer – Cyberia (апрель 2024 - настоящее время)

-

Дорабатываю разные штуки.

+
+

{t('experience.title')}

+

{t('experience.job1.title')} – {t('experience.job1.company')}

+

{t('experience.job1.description')}

-
-

Образование

-

РТУ МИРЭА (2025) – Институт кибербезопасности и цифровых технологий, Информационные системы и технологии

+
+

{t('education.title')}

+

{t('education.university.name')} ({t('education.university.year')}) – {t('education.university.faculty')}, {t('education.university.specialty')}

-
-

Навыки

-

TypeScript, JavaScript, React, Git, Node.js, HTML5, CSS3, SOLID, Redux, ООП, SCSS, BEM

+
+

{t('skills.title')}

+
+ {skillList.map(skill => ( + {skill} + ))} +
-
-

Проекты

-

Все мои проекты вы можете оценить на GitHub.

+
+

{t('projects.title')}

+

+ {t.rich('projects.description', { + githubLink: (chunks) => {chunks} + })} +

-