Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion backend/src/endpoints/api/spotify/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize};
pub struct NowPlayingResponse {
pub item: Option<Item>,
pub is_playing: bool,
pub device: Option<Device>,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
Expand Down Expand Up @@ -34,6 +35,12 @@ pub struct ExternalUrls {
pub spotify: String,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Device {
pub volume_percent: Option<u32>,
}


#[derive(Debug, Deserialize)]
pub struct TokenResponse {
pub access_token: String,
Expand Down Expand Up @@ -61,4 +68,4 @@ pub struct NowPlayingStreamData {
#[serde(rename = "songUrl")]
#[serde(skip_serializing_if = "Option::is_none")]
pub song_url: Option<String>,
}
}
22 changes: 19 additions & 3 deletions backend/src/endpoints/api/spotify/services.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -46,7 +47,22 @@ impl SpotifyService {

match status {
StatusCode::OK => {
let now_playing_response = response.json::<NowPlayingResponse>().await?;
// Делаем переменную изменяемой, чтобы модифицировать ее
let mut now_playing_response = response.json::<NowPlayingResponse>().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 => {
Expand Down Expand Up @@ -129,4 +145,4 @@ impl SpotifyService {
Err(AppError::InternalError(error_message))
}
}
}
}
104 changes: 81 additions & 23 deletions frontend/app/[locale]/about/page.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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) {
Expand All @@ -115,4 +173,4 @@
.sidebar {
max-width: 100%;
}
}
}
119 changes: 88 additions & 31 deletions frontend/app/[locale]/about/page.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLElement[]>([]);
const [entries, setEntries] = useState<IntersectionObserverEntry[]>([]);
const observer = useRef<IntersectionObserver | null>(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 (
<div className={styles.container}>
<header className={styles.header}>
<h1>Хохлов Дмитрий</h1>
<p>FullStack Developer (TypeScript, Rust)</p>
<header className={`${styles.header} ${styles.animatedSection}`}>
<h1>{t('name')}</h1>
<p>{t('role')}</p>
<button onClick={() => setShowContact(!showContact)} className={styles.contactButton}>
{showContact ? 'Скрыть контакты' : 'Показать контакты'}
{showContact ? t('hideContact') : t('showContact')}
</button>
{showContact && (
<div className={styles.contactDetails}>
<p>Телефон: +7 (932) 477-0975</p>
<p>Email: hohlov.03@inbox.ru</p>
<p>Telegram: @diametrfq</p>
<a href="tel:+79324770975" className={styles.contactLink}>
<span className="material-symbols-outlined">call</span>
{t('contact.phone')}
</a>
<a href="mailto:hohlov.03@inbox.ru" className={styles.contactLink}>
<span className="material-symbols-outlined">email</span>
{t('contact.email')}
</a>
<a href="https://t.me/diametrfq" target="_blank" rel="noopener noreferrer" className={styles.contactLink}>
{t('contact.telegram')}
</a>
</div>
)}
</header>

<div className={styles.contentWrapper}>
<main className={styles.mainContent}>
<section>
<h2>Обо мне</h2>
<p>
Я опытный FullStack разработчик с более чем четырехлетним опытом в создании
эффективных веб-приложений. Мои навыки охватывают весь цикл разработки, от концепции и дизайна до реализации и поддержки.
</p>
<p>
Активно следую за последними тенденциями веб-разработки, увлечен созданием чистого, эффективного и масштабируемого кода.
</p>
<section className={`${styles.card} ${styles.animatedSection}`}>
<h2>{t('about.title')}</h2>
<p>{t('about.paragraph1')}</p>
<p>{t('about.paragraph2')}</p>
</section>

<section>
<h2>Опыт работы</h2>
<p><strong>Junior FullStack Developer</strong> – Cyberia (апрель 2024 - настоящее время)</p>
<p>Дорабатываю разные штуки.</p>
<section className={`${styles.card} ${styles.animatedSection}`}>
<h2>{t('experience.title')}</h2>
<p><strong>{t('experience.job1.title')}</strong> – {t('experience.job1.company')}</p>
<p>{t('experience.job1.description')}</p>
</section>

<section>
<h2>Образование</h2>
<p><strong>РТУ МИРЭА</strong> (2025)Институт кибербезопасности и цифровых технологий, Информационные системы и технологии</p>
<section className={`${styles.card} ${styles.animatedSection}`}>
<h2>{t('education.title')}</h2>
<p><strong>{t('education.university.name')}</strong> ({t('education.university.year')}){t('education.university.faculty')}, {t('education.university.specialty')}</p>
</section>

<section>
<h2>Навыки</h2>
<p>TypeScript, JavaScript, React, Git, Node.js, HTML5, CSS3, SOLID, Redux, ООП, SCSS, BEM</p>
<section className={`${styles.card} ${styles.animatedSection}`}>
<h2>{t('skills.title')}</h2>
<div className={styles.skillsContainer}>
{skillList.map(skill => (
<span key={skill} className={styles.skillTag}>{skill}</span>
))}
</div>
</section>

<section>
<h2>Проекты</h2>
<p>Все мои проекты вы можете оценить на <Link href="https://github.com/DiametrFQ" target="_blank" className={styles.link}>GitHub</Link>.</p>
<section className={`${styles.card} ${styles.animatedSection}`}>
<h2>{t('projects.title')}</h2>
<p>
{t.rich('projects.description', {
githubLink: (chunks) => <Link href="https://github.com/DiametrFQ" target="_blank" className={styles.link}>{chunks}</Link>
})}
</p>
</section>
</main>

<aside className={styles.sidebar}>
<aside className={`${styles.sidebar} ${styles.animatedSection}`}>
<div className={styles.statsImages}>
<Image src="https://www.codewars.com/users/DiametrFQ/badges/small" width={300} height={54} alt='Codewars stats' unoptimized/>
<Image src="https://streak-stats.demolab.com?user=DiametrFQ&theme=github-dark-blue&border_radius=6&card_width=300&type=png" width={300} height={150} alt="GitHub Streak" unoptimized/>
Expand Down
Loading