diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 0f85145..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: CI - Build and Test - -on: - pull_request: - -jobs: - build_and_test: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: '22' - cache: 'npm' - cache-dependency-path: frontend/package-lock.json - - - name: Install Frontend Dependencies - working-directory: ./frontend - run: npm ci - - - name: Build Frontend (for verification) - working-directory: ./frontend - run: npm run build - - - name: Set up Rust - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - profile: minimal - override: true - - - name: Set up Rust cache - uses: Swatinem/rust-cache@v2 - - - name: Check Backend code - working-directory: ./backend - run: cargo check --verbose - - - name: Build Backend (for verification) - working-directory: ./backend - run: cargo build --verbose diff --git a/frontend/app/[locale]/contact/page.module.css b/frontend/app/[locale]/contact/page.module.css index ee9b8e6..8596e1b 100644 --- a/frontend/app/[locale]/contact/page.module.css +++ b/frontend/app/[locale]/contact/page.module.css @@ -1,168 +1,119 @@ -.page { - --gray-rgb: 0, 0, 0; - --gray-alpha-200: rgba(var(--gray-rgb), 0.08); - --gray-alpha-100: rgba(var(--gray-rgb), 0.05); - - --button-primary-hover: #383838; - --button-secondary-hover: #f2f2f2; - - display: grid; - grid-template-rows: 20px 1fr 20px; - align-items: center; - justify-items: center; - min-height: 100svh; - padding: 80px; - gap: 64px; - font-family: var(--font-geist-sans); +.container { + max-width: 960px; + margin: 40px auto; + padding: 20px; + text-align: center; } -@media (prefers-color-scheme: dark) { - .page { - --gray-rgb: 255, 255, 255; - --gray-alpha-200: rgba(var(--gray-rgb), 0.145); - --gray-alpha-100: rgba(var(--gray-rgb), 0.06); - - --button-primary-hover: #ccc; - --button-secondary-hover: #1a1a1a; - } -} - -.main { - display: flex; - flex-direction: column; - gap: 32px; - grid-row-start: 2; -} - -.main ol { - font-family: var(--font-geist-mono); - padding-left: 0; - margin: 0; - font-size: 14px; - line-height: 24px; - letter-spacing: -0.01em; - list-style-position: inside; +.title { + font-size: 2.5rem; + font-weight: bold; + margin-bottom: 1rem; + color: var(--foreground); } -.main li:not(:last-of-type) { - margin-bottom: 8px; +.subtitle { + font-size: 1.2rem; + color: var(--muted-foreground); + margin-bottom: 3rem; } -.main code { - font-family: inherit; - background: var(--gray-alpha-100); - padding: 2px 4px; - border-radius: 4px; - font-weight: 600; -} - -.ctas { - display: flex; - gap: 16px; +.grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 1.5rem; + text-align: left; } -.ctas a { - appearance: none; - border-radius: 128px; - height: 48px; - padding: 0 20px; - border: none; - border: 1px solid transparent; - transition: - background 0.2s, - color 0.2s, - border-color 0.2s; - cursor: pointer; +.card { display: flex; align-items: center; - justify-content: center; - font-size: 16px; - line-height: 20px; - font-weight: 500; + gap: 1.5rem; + background-color: var(--card); + padding: 1.5rem; + border-radius: 12px; + border: 1px solid var(--border); + transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out; + text-decoration: none; + color: inherit; } -a.primary { - background: var(--foreground); - color: var(--background); - gap: 8px; +.card:hover { + transform: translateY(-5px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1); } -a.secondary { - border-color: var(--gray-alpha-200); - min-width: 180px; +.dark .card:hover { + box-shadow: 0 8px 25px rgba(255, 255, 255, 0.08); } -.footer { - grid-row-start: 3; - display: flex; - gap: 24px; -} - -.footer a { +.iconWrapper { + flex-shrink: 0; display: flex; align-items: center; - gap: 8px; + justify-content: center; + width: 64px; + height: 64px; + /* Для светлой темы фон остается прежним */ + background-color: var(--muted); + border-radius: 50%; + transition: background-color 0.3s ease-in-out; +} + +/* + НОВЫЙ БЛОК: Правило для фона иконки в темной теме. + Вместо светло-серого мы используем var(--secondary), + который является темно-серым и лучше вписывается в фон карточки. +*/ +.dark .iconWrapper { + background-color: var(--secondary); +} + +.icon { + width: 40px; + height: 40px; + object-fit: contain; + /* Добавляем плавный переход для фильтра и прозрачности */ + transition: filter 0.3s ease-in-out, opacity 0.3s ease-in-out; +} + +/* + ИЗМЕНЕННЫЙ БЛОК: Улучшенная адаптация иконки. + Мы по-прежнему инвертируем ее, чтобы сделать белой, + но добавляем прозрачность. Это убирает резкость и делает + иконку похожей по яркости на основной текст. +*/ +.dark .invertOnDark { + /* filter: brightness(0); + opacity: 1; */ + color: white; + background-color: white; +} + +.cardContent { + display: flex; + flex-direction: column; } -.footer img { - flex-shrink: 0; +.cardTitle { + font-size: 1.25rem; + font-weight: 600; + color: var(--foreground); + margin: 0; } -/* Enable hover only on non-touch devices */ -@media (hover: hover) and (pointer: fine) { - a.primary:hover { - background: var(--button-primary-hover); - border-color: transparent; - } - - a.secondary:hover { - background: var(--button-secondary-hover); - border-color: transparent; - } - - .footer a:hover { - text-decoration: underline; - text-underline-offset: 4px; - } +.cardHandle { + font-size: 1rem; + color: var(--muted-foreground); + margin: 0; + word-break: break-all; } -@media (max-width: 600px) { - .page { - padding: 32px; - padding-bottom: 80px; +@media (max-width: 640px) { + .title { + font-size: 2rem; } - - .main { - align-items: center; - } - - .main ol { - text-align: center; + .grid { + grid-template-columns: 1fr; } - - .ctas { - flex-direction: column; - } - - .ctas a { - font-size: 14px; - height: 40px; - padding: 0 16px; - } - - a.secondary { - min-width: auto; - } - - .footer { - flex-wrap: wrap; - align-items: center; - justify-content: center; - } -} - -@media (prefers-color-scheme: dark) { - .logo { - filter: invert(); - } -} +} \ No newline at end of file diff --git a/frontend/app/[locale]/contact/page.tsx b/frontend/app/[locale]/contact/page.tsx index 5ed795d..cbe76d3 100644 --- a/frontend/app/[locale]/contact/page.tsx +++ b/frontend/app/[locale]/contact/page.tsx @@ -1,9 +1,75 @@ -import styles from "./page.module.css"; +'use client'; + +import Image from 'next/image'; +import { useTranslations } from 'next-intl'; +import styles from './page.module.css'; + +const contacts = [ + { + name: 'Telegram', + handle: '@diametrfq', + url: 'https://t.me/diametrfq', + iconUrl: '/logo/telegram.svg', + }, + { + name: 'LinkedIn', + handle: 'Dmitry Khokhlov', + url: 'https://www.linkedin.com/in/diametrfq', + iconUrl: '/logo/linkedin.svg', + }, + { + name: 'GitHub', + handle: 'DiametrFQ', + url: 'https://github.com/DiametrFQ', + iconUrl: 'https://cdn.worldvectorlogo.com/logos/github-icon-1.svg', + themeBehavior: 'invertOnDark', + }, + { + name: 'Email', + handle: 'hohlov.03@inbox.ru', + url: 'mailto:hohlov.03@inbox.ru', + iconUrl: '/logo/email.png', + }, + { + name: 'Steam', + handle: 'diametrfq', + url: 'https://steamcommunity.com/id/diametrfq/', + iconUrl: '/logo/steam.png', + }, +]; + +export default function Contact() { + const t = useTranslations('ContactPage'); -export default function Portfolio() { return ( -
- contact +
+

{t('title')}

+

{t('subtitle')}

+
+ {contacts.map((contact) => ( + +
+ {`${contact.name} +
+
+

{contact.name}

+

{contact.handle}

+
+
+ ))} +
); -} +} \ No newline at end of file diff --git a/frontend/app/api/spotify-stream/route.ts b/frontend/app/api/spotify-stream/route.ts index 0f685fa..05e00b4 100644 --- a/frontend/app/api/spotify-stream/route.ts +++ b/frontend/app/api/spotify-stream/route.ts @@ -1,60 +1,40 @@ import { NextRequest } from 'next/server'; -import http from 'http'; export const dynamic = 'force-dynamic'; export async function GET(request: NextRequest) { - const backendUrl = process.env.RUST_BACKEND_URL || 'http://localhost:8080'; - - const url = new URL(backendUrl); - const options = { - hostname: url.hostname, - port: url.port, - path: '/api/spotify/now_playing_stream', - method: 'GET', - headers: { - 'Accept': 'text/event-stream', - 'Connection': 'keep-alive', - }, - }; + const backendUrl = (process.env.RUST_BACKEND_URL || 'http://localhost:8080') + '/api/spotify/now_playing_stream'; - const stream = new ReadableStream({ - start(controller) { - const backendRequest = http.get(options, (backendResponse) => { - backendResponse.on('data', (chunk) => { - controller.enqueue(chunk); - }); + try { + const backendResponse = await fetch(backendUrl, { + signal: request.signal, + headers: { + 'Accept': 'text/event-stream', + }, + }); - backendResponse.on('end', () => { - console.log('Backend stream ended.'); - controller.close(); - }); - - backendResponse.on('error', (err) => { - console.error('Error in backend response stream:', err); - controller.error(err); - }); + if (!backendResponse.ok) { + return new Response(backendResponse.body, { + status: backendResponse.status, + statusText: backendResponse.statusText, }); + } - backendRequest.on('error', (err) => { - console.error('Error making request to backend:', err); - controller.error(err); - }); - - request.signal.addEventListener('abort', () => { - console.log('Client aborted request. Destroying backend request.'); - backendRequest.destroy(); - controller.close(); - }); - }, - }); + return new Response(backendResponse.body, { + status: 200, + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + }, + }); - return new Response(stream, { - status: 200, - headers: { - 'Content-Type': 'text/event-stream', - 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive', - }, - }); + } catch (error) { + if (error instanceof Error && error.name === 'AbortError') { + console.log('Stream request aborted by client.'); + return new Response('Stream aborted', { status: 499 }); + } + console.error('Error proxying spotify stream:', error); + return new Response('Failed to proxy stream', { status: 500 }); + } } \ No newline at end of file diff --git a/frontend/locales/en.json b/frontend/locales/en.json index d8aa3e6..fa3ffe9 100644 --- a/frontend/locales/en.json +++ b/frontend/locales/en.json @@ -15,5 +15,9 @@ "telegram":{ "title": "Telegram" } + }, + "ContactPage": { + "title": "Get in Touch", + "subtitle": "Here is where you can find me." } } \ No newline at end of file diff --git a/frontend/locales/ru.json b/frontend/locales/ru.json index b346b82..756392c 100644 --- a/frontend/locales/ru.json +++ b/frontend/locales/ru.json @@ -15,5 +15,9 @@ "telegram":{ "title": "Telegram" } + }, + "ContactPage": { + "title": "Свяжитесь со мной", + "subtitle": "Вот лучшие способы для связи." } } \ No newline at end of file diff --git a/frontend/next.config.ts b/frontend/next.config.ts index 81e46fe..d2e437d 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -41,6 +41,22 @@ const nextConfig: NextConfig = { { protocol: 'https', hostname: 'i.scdn.co', + }, + { + protocol: 'https', + hostname: 'cdn-icons-png.flaticon.com', + }, + { + protocol: 'https', + hostname: 'upload.wikimedia.org', + }, + { + protocol: 'https', + hostname: 'static-00.iconduck.com', + }, + { + protocol: 'https', + hostname: 'cdn.worldvectorlogo.com', }, ], }, diff --git a/frontend/public/logo/email.png b/frontend/public/logo/email.png new file mode 100644 index 0000000..81fc0b4 Binary files /dev/null and b/frontend/public/logo/email.png differ diff --git a/frontend/public/logo/github.png b/frontend/public/logo/github.png new file mode 100644 index 0000000..df53e88 Binary files /dev/null and b/frontend/public/logo/github.png differ diff --git a/frontend/public/logo/linkedin.svg b/frontend/public/logo/linkedin.svg new file mode 100644 index 0000000..001bdcd --- /dev/null +++ b/frontend/public/logo/linkedin.svg @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/frontend/public/logo/steam.png b/frontend/public/logo/steam.png new file mode 100644 index 0000000..12094a5 Binary files /dev/null and b/frontend/public/logo/steam.png differ diff --git a/frontend/public/logo/telegram.svg b/frontend/public/logo/telegram.svg new file mode 100644 index 0000000..6f88d42 --- /dev/null +++ b/frontend/public/logo/telegram.svg @@ -0,0 +1 @@ +Telegram_logo \ No newline at end of file