diff --git a/README.md b/README.md index e215bc4..d1f74b0 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,7 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). - ## Getting Started -First, run the development server: - -```bash +```bash для старта сервера npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +Open [http://localhost:3000](http://localhost:3000) \ No newline at end of file diff --git a/src/app/api/action.ts b/src/app/api/action.ts index 832820c..be669f3 100644 --- a/src/app/api/action.ts +++ b/src/app/api/action.ts @@ -1,5 +1,19 @@ 'use server' +const FRUITS_URL = 'https://www.fruityvice.com/api/fruit/'; +const ALL = 'all' + +export async function fetchFruits() { + try { + const response = await fetch(FRUITS_URL + ALL, { 'method': 'GET' }); + let parsed = await response.json(); + return parsed; + } catch (error) { + console.error('Error:', error); + throw new Error('Failed to fetch fruits data.'); + } +} + export async function testConnect() { try { const response = await fetch('http://localhost:8080/api/auth/register', { diff --git a/src/app/hooks/useClickOutside.tsx b/src/app/hooks/useClickOutside.tsx new file mode 100644 index 0000000..afa2271 --- /dev/null +++ b/src/app/hooks/useClickOutside.tsx @@ -0,0 +1,24 @@ +'use client'; + +import { useEffect, RefObject } from 'react'; + +export function useClickOutside( + ref: RefObject, + callback: () => void +) { + useEffect(() => { + const handleClick = (event: MouseEvent) => { + if (!ref) { + return; + } + if (ref.current && !ref.current.contains(event.target as Node)) { + callback(); + } + }; + + document.addEventListener('mousedown', handleClick); + return () => { + document.removeEventListener('mousedown', handleClick); + }; + }, [ref, callback]); +} \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 82dfa6b..e066a36 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; -import "./globals.css"; +import "./styles/globals.css"; +import "./styles/global-styles.scss" export const metadata: Metadata = { title: "No copy", diff --git a/src/app/page.module.scss b/src/app/page.module.scss index bbb38d4..7b41cb8 100644 --- a/src/app/page.module.scss +++ b/src/app/page.module.scss @@ -1,3 +1,7 @@ .mainContainter { margin-left: 280px; + flex: 1; + padding: 30px; + background: #f8f9ff; + min-height: 100vh; } \ No newline at end of file diff --git a/src/app/pages/dashboard/page.tsx b/src/app/pages/dashboard/page.tsx index b7a8640..92fa054 100644 --- a/src/app/pages/dashboard/page.tsx +++ b/src/app/pages/dashboard/page.tsx @@ -1,5 +1,7 @@ import { Metadata } from 'next'; -import Button from '@/app/ui/button' +import { Suspense } from 'react'; +import ButtonTest from '@/app/ui/button-test' +import ProtectionOverview from '@/app/ui/dashboard/protection-overview'; export const metadata: Metadata = { title: 'Dashboard', @@ -9,10 +11,11 @@ export default function Home() { return (
-
- Dashboard -
-
); diff --git a/src/app/pages/layout.tsx b/src/app/pages/layout.tsx index 3b401bf..1ef3c00 100644 --- a/src/app/pages/layout.tsx +++ b/src/app/pages/layout.tsx @@ -1,11 +1,17 @@ import NavLinks from '@/app/ui/nav-links' import styles from '@/app/page.module.scss' +import HeaderPanel from '../ui/header/headerPanel'; export default function Layout({ children }: { children: React.ReactNode }) { return (
-
{children}
+
+ +
+ {children} +
+
); } \ No newline at end of file diff --git a/src/app/styles/dashboard.scss b/src/app/styles/dashboard.scss new file mode 100644 index 0000000..42ee070 --- /dev/null +++ b/src/app/styles/dashboard.scss @@ -0,0 +1,30 @@ +.page-title { + font-size: 28px; + font-weight: 600; + margin-bottom: 30px; + display: flex; + align-items: center; + color: #1f2937; + + &::before { + content: ''; + width: 8px; + height: 8px; + background: #6366f1; + border-radius: 50%; + margin-right: 15px; + } +} + +.protection-overview { + h3 { + margin: 0 0 10px 0; + font-size: 20px; + } + + p { + margin: 0; + opacity: 0.9; + font-size: 14px; + } +} \ No newline at end of file diff --git a/src/app/styles/global-styles.scss b/src/app/styles/global-styles.scss new file mode 100644 index 0000000..e630dee --- /dev/null +++ b/src/app/styles/global-styles.scss @@ -0,0 +1,2 @@ +@use './header.scss'; +@use './dashboard.scss'; \ No newline at end of file diff --git a/src/app/globals.css b/src/app/styles/globals.css similarity index 100% rename from src/app/globals.css rename to src/app/styles/globals.css diff --git a/src/app/styles/header.scss b/src/app/styles/header.scss new file mode 100644 index 0000000..0a76e91 --- /dev/null +++ b/src/app/styles/header.scss @@ -0,0 +1,306 @@ +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30px; + background: white; + padding: 20px 30px; + border-radius: 20px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); + + .header-action { + display: flex; + align-items: center; + gap: 15px; + + .icon-btn { + width: 40px; + height: 40px; + border-radius: 50%; + background: #f8f9fa; + border: none; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s; + position: relative; + + &:hover { + background: #e9ecef; + transform: translateY(-2px); + } + } + } + + + .search-bar { + display: flex; + align-items: center; + background: #f8f9fa; + border-radius: 20px; + padding: 10px 20px; + width: 300px; + transition: all 0.3s; + + input { + border: none; + outline: none; + flex: 1; + margin-left: 10px; + font-size: 14px; + background: transparent; + } + } + + .notification-wrapper, + .user-menu, + .tokens-balance { + position: relative; + } + + .tokens-balance { + margin-right: 15px; + } + + .tokens-wrapper { + display: flex; + align-items: center; + gap: 6px; + background: white; + color: #6366f1; + border: 2px solid #6366f1; + padding: 8px 12px; + border-radius: 20px; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 4px 12px rgba(99, 102, 241, 0.15); + font-size: 12px; + font-weight: 600; + min-width: 110px; + } + + .tokens-count { + font-weight: 700; + font-size: 14px; + letter-spacing: 0.5px; + } + + .tokens-label { + font-weight: 500; + opacity: 0.9; + } + + .notification-dropdown, + .user-dropdown { + position: absolute; + top: calc(100% + 10px); + right: 0; + background: white; + border-radius: 12px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15); + min-width: 320px; + opacity: 0; + visibility: hidden; + transform: translateY(-10px); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + z-index: 1000; + + &.show { + opacity: 1; + visibility: visible; + transform: translateY(0); + } + } + + .notification-dropdown { + .notification-header { + padding: 15px 20px; + border-bottom: 1px solid #f3f4f6; + display: flex; + justify-content: space-between; + align-items: center; + + h4 { + margin: 0; + font-size: 16px; + font-weight: 600; + color: #1f2937; + } + + .notification-count { + font-size: 12px; + color: #6366f1; + font-weight: 500; + } + } + + .notification-list { + max-height: 400px; + overflow-y: auto; + } + + .notification-item { + display: flex; + gap: 12px; + padding: 15px 20px; + border-bottom: 1px solid #f9fafb; + transition: background-color 0.2s; + cursor: pointer; + } + + .notification-title { + display: block; + font-size: 15px; + font-weight: 700; + color: #1f2937; + margin-bottom: 6px; + } + + .notification-icon { + font-size: 24px; + flex-shrink: 0; + } + + .notification-text { + font-size: 13px; + color: #4b5563; + margin-bottom: 4px; + font-weight: 500; + } + + .notification-time { + font-size: 12px; + color: #9ca3af; + } + + .notification-footer { + padding: 12px 20px; + border-top: 1px solid #f3f4f6; + text-align: center; + + .notification-link { + color: #6366f1; + font-size: 14px; + font-weight: 500; + text-decoration: none; + } + } + } + + .user-menu { + .user-avatar { + width: 40px; + height: 40px; + border-radius: 50%; + background: linear-gradient(45deg, #6366f1, #8b5cf6); + display: flex; + align-items: center; + justify-content: center; + color: white; + font-weight: 600; + cursor: pointer; + transition: all 0.3s; + } + + .user-dropdown { + min-width: 280px; + } + + .user-info-header { + padding: 20px; + border-bottom: 1px solid #f3f4f6; + display: flex; + gap: 15px; + align-items: center; + } + + .user-avatar-large { + width: 50px; + height: 50px; + background: linear-gradient(135deg, #6366f1, #8b5cf6); + color: white; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + font-size: 20px; + flex-shrink: 0; + } + + .user-details { + flex: 1; + min-width: 0; + } + + .user-name { + font-weight: 600; + font-size: 15px; + color: #1f2937; + margin-bottom: 4px; + } + + .user-email { + font-size: 13px; + color: #6b7280; + margin-bottom: 8px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .user-subscription { + display: inline-block; + } + + .user-menu-items { + padding: 10px 0; + } + + .user-menu-item { + display: flex; + align-items: center; + padding: 12px 20px; + text-decoration: none; + color: #374151; + transition: background-color 0.2s; + + svg { + width: 18px; + height: 18px; + margin-right: 12px; + } + } + + .subscription-badge { + display: inline-block; + padding: 4px 10px; + border-radius: 12px; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + .user-menu-footer { + border-top: 1px solid #f3f4f6; + padding: 10px 0; + } + + .user-menu-logout { + display: flex; + align-items: center; + padding: 12px 20px; + text-decoration: none; + color: #dc2626; + transition: background-color 0.2s; + + svg { + width: 18px; + height: 18px; + margin-right: 12px; + } + } + } +} \ No newline at end of file diff --git a/src/app/ui/button.tsx b/src/app/ui/button-test.tsx similarity index 62% rename from src/app/ui/button.tsx rename to src/app/ui/button-test.tsx index f8de89f..8d54224 100644 --- a/src/app/ui/button.tsx +++ b/src/app/ui/button-test.tsx @@ -3,13 +3,12 @@ import { testConnect } from '@/app/api/action'; async function clickHandler() { - console.log('click'); testConnect(); } -export default function Button() { +export default function ButtonTest() { return ( - ); diff --git a/src/app/ui/dashboard/protection-overview.tsx b/src/app/ui/dashboard/protection-overview.tsx new file mode 100644 index 0000000..87abba4 --- /dev/null +++ b/src/app/ui/dashboard/protection-overview.tsx @@ -0,0 +1,8 @@ +export default function ProtectionOverview() { + return ( +
+

Защита вашего контента

+

Текущий статус защищенности и активности системы мониторинга

+
+ ) +} \ No newline at end of file diff --git a/src/app/ui/header/headerPanel.tsx b/src/app/ui/header/headerPanel.tsx new file mode 100644 index 0000000..3cc3e3e --- /dev/null +++ b/src/app/ui/header/headerPanel.tsx @@ -0,0 +1,38 @@ +import NotificationsButton from './notificationsButton'; +import UserMenuButton from './userMenuButton'; +import Link from 'next/link'; + +export default function HeaderPanel() { + return ( +
+
+ + + + +
+
+
+
+ + + + 800 + токенов +
+
+ + + + + + + +
+
+ ) +} \ No newline at end of file diff --git a/src/app/ui/header/notificationsButton.tsx b/src/app/ui/header/notificationsButton.tsx new file mode 100644 index 0000000..6eaeb10 --- /dev/null +++ b/src/app/ui/header/notificationsButton.tsx @@ -0,0 +1,87 @@ +"use client" + +import { useState, useRef } from 'react'; +import { fetchFruits } from '@/app/api/action'; +import { useClickOutside } from '@/app/hooks/useClickOutside'; +import Link from 'next/link'; + +export default function NotificationsButton() { + const [isOpen, setIsOpen] = useState(false); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const menuRef = useRef(null); + + + useClickOutside(menuRef, () => { + setIsOpen(false); + }); + + const fetchData = async () => { + setLoading(true); + setError(null); + + try { + const result = await fetchFruits(); + setData("result"); + setIsOpen(true); + } catch (err) { + setError('error message'); + } finally { + setLoading(false); + } + }; + + const handleButtonClick = () => { + if (!isOpen) { + fetchData(); + } else { + setIsOpen(false); + } + }; + + return ( +
+ +
+ {error ? ( +
{error}
+ ) : ( + <> +
+

Уведомления

+ Нет новых +
+ {data ? ( +
+
+
+
+
🪙 Токены добавлены
+
На ваш баланс добавлено 100 токенов. Теперь вы можете защитить ещё больше файлов!
+
4 дн назад
+
+
+ {JSON.stringify(data)} +
+ ) : ( +

Нет данных

+ )} +
+ Посмотреть все +
+ + )} +
+
+ ) +} \ No newline at end of file diff --git a/src/app/ui/header/userMenuButton.tsx b/src/app/ui/header/userMenuButton.tsx new file mode 100644 index 0000000..1f31572 --- /dev/null +++ b/src/app/ui/header/userMenuButton.tsx @@ -0,0 +1,84 @@ +'use client' + +import { useState, useRef } from 'react'; + +import { useClickOutside } from '@/app/hooks/useClickOutside'; +import Link from 'next/link'; +export default function UserMenuButton() { + const [isOpen, setIsOpen] = useState(false); + const menuRef = useRef(null); + + + useClickOutside(menuRef, () => { + setIsOpen(false); + }); + + const handleButtonClick = () => { + if (!isOpen) { + setIsOpen(true); + } else { + setIsOpen(false); + } + }; + + return ( +
+
+ User +
+
+
+
User
+
+
Валерка
+
va@no-copy.ru
+
+ + СТАРТ +
+
+
+ +
+ + + + + Главная + + + + + + Настройки + + + + + + Мой контент + + + + + + Помощь + +
+ +
+ + + + + Выйти + +
+
+
+ ); +}; \ No newline at end of file