Добавлены новые зависимости, обновлены стили и улучшена структура проекта. Обновлен README с описанием функционала и технологий. Реализована анимация и адаптивный дизайн. Настроена авторизация с использованием Apollo Client.
This commit is contained in:
65
src/app/api/graphql/route.ts
Normal file
65
src/app/api/graphql/route.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import { ApolloServer } from '@apollo/server'
|
||||
import { startServerAndCreateNextHandler } from '@as-integrations/next'
|
||||
import { NextRequest } from 'next/server'
|
||||
import jwt from 'jsonwebtoken'
|
||||
import { typeDefs } from '@/graphql/typedefs'
|
||||
import { resolvers } from '@/graphql/resolvers'
|
||||
|
||||
// Интерфейс для контекста
|
||||
interface Context {
|
||||
user?: {
|
||||
id: string
|
||||
phone: string
|
||||
}
|
||||
}
|
||||
|
||||
// Создаем Apollo Server
|
||||
const server = new ApolloServer<Context>({
|
||||
typeDefs,
|
||||
resolvers,
|
||||
})
|
||||
|
||||
// Создаем Next.js handler
|
||||
const handler = startServerAndCreateNextHandler<NextRequest>(server, {
|
||||
context: async (req: NextRequest) => {
|
||||
// Извлекаем токен из заголовка Authorization
|
||||
const authHeader = req.headers.get('authorization')
|
||||
const token = authHeader?.replace('Bearer ', '')
|
||||
|
||||
console.log('GraphQL Context - Auth header:', authHeader)
|
||||
console.log('GraphQL Context - Token:', token ? `${token.substring(0, 20)}...` : 'No token')
|
||||
|
||||
if (!token) {
|
||||
console.log('GraphQL Context - No token provided')
|
||||
return { user: undefined }
|
||||
}
|
||||
|
||||
try {
|
||||
// Верифицируем JWT токен
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as {
|
||||
userId: string
|
||||
phone: string
|
||||
}
|
||||
|
||||
console.log('GraphQL Context - Decoded user:', { id: decoded.userId, phone: decoded.phone })
|
||||
|
||||
return {
|
||||
user: {
|
||||
id: decoded.userId,
|
||||
phone: decoded.phone
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('GraphQL Context - Invalid token:', error)
|
||||
return { user: undefined }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
return handler(request)
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
return handler(request)
|
||||
}
|
104
src/app/api/upload-avatar/route.ts
Normal file
104
src/app/api/upload-avatar/route.ts
Normal file
@ -0,0 +1,104 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'
|
||||
|
||||
const s3Client = new S3Client({
|
||||
region: 'ru-1',
|
||||
endpoint: 'https://s3.twcstorage.ru',
|
||||
credentials: {
|
||||
accessKeyId: 'I6XD2OR7YO2ZN6L6Z629',
|
||||
secretAccessKey: '9xCOoafisG0aB9lJNvdLO1UuK73fBvMcpHMdijrJ'
|
||||
},
|
||||
forcePathStyle: true
|
||||
})
|
||||
|
||||
const BUCKET_NAME = '617774af-sfera'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const formData = await request.formData()
|
||||
const file = formData.get('file') as File
|
||||
const key = formData.get('key') as string
|
||||
|
||||
if (!file || !key) {
|
||||
return NextResponse.json(
|
||||
{ error: 'File and key are required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Проверяем тип файла
|
||||
if (!file.type.startsWith('image/')) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Only image files are allowed' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Ограничиваем размер файла (5MB)
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
return NextResponse.json(
|
||||
{ error: 'File size must be less than 5MB' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Конвертируем файл в Buffer
|
||||
const buffer = Buffer.from(await file.arrayBuffer())
|
||||
|
||||
// Загружаем в S3
|
||||
const command = new PutObjectCommand({
|
||||
Bucket: BUCKET_NAME,
|
||||
Key: key,
|
||||
Body: buffer,
|
||||
ContentType: file.type,
|
||||
ACL: 'public-read'
|
||||
})
|
||||
|
||||
await s3Client.send(command)
|
||||
|
||||
// Возвращаем URL файла
|
||||
const url = `https://s3.twcstorage.ru/${BUCKET_NAME}/${key}`
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
url,
|
||||
key
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error uploading avatar:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to upload avatar' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
const { key } = await request.json()
|
||||
|
||||
if (!key) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Key is required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: Добавить удаление из S3
|
||||
// const command = new DeleteObjectCommand({
|
||||
// Bucket: BUCKET_NAME,
|
||||
// Key: key
|
||||
// })
|
||||
// await s3Client.send(command)
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error deleting avatar:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to delete avatar' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
10
src/app/dashboard/page.tsx
Normal file
10
src/app/dashboard/page.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { AuthGuard } from "@/components/auth-guard"
|
||||
import { DashboardHome } from "@/components/dashboard/dashboard-home"
|
||||
|
||||
export default function DashboardPage() {
|
||||
return (
|
||||
<AuthGuard>
|
||||
<DashboardHome />
|
||||
</AuthGuard>
|
||||
)
|
||||
}
|
@ -1,26 +1,378 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(0.98 0.02 320);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.65 0.28 315);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.94 0.08 315);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.94 0.05 315);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.90 0.12 315);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.90 0.08 315);
|
||||
--input: oklch(0.96 0.05 315);
|
||||
--ring: oklch(0.65 0.28 315);
|
||||
--chart-1: oklch(0.70 0.25 315);
|
||||
--chart-2: oklch(0.65 0.22 290);
|
||||
--chart-3: oklch(0.60 0.20 340);
|
||||
--chart-4: oklch(0.75 0.18 305);
|
||||
--chart-5: oklch(0.68 0.24 325);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.65 0.28 315);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.90 0.12 315);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.90 0.08 315);
|
||||
--sidebar-ring: oklch(0.65 0.28 315);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.08 0.08 315);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.12 0.08 315);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.12 0.08 315);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.75 0.32 315);
|
||||
--primary-foreground: oklch(0.08 0.08 315);
|
||||
--secondary: oklch(0.18 0.12 315);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.18 0.10 315);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.20 0.15 315);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(0.22 0.12 315);
|
||||
--input: oklch(0.15 0.10 315);
|
||||
--ring: oklch(0.75 0.32 315);
|
||||
--chart-1: oklch(0.75 0.32 315);
|
||||
--chart-2: oklch(0.70 0.28 290);
|
||||
--chart-3: oklch(0.65 0.25 340);
|
||||
--chart-4: oklch(0.80 0.20 305);
|
||||
--chart-5: oklch(0.72 0.30 325);
|
||||
--sidebar: oklch(0.12 0.08 315);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.75 0.32 315);
|
||||
--sidebar-primary-foreground: oklch(0.08 0.08 315);
|
||||
--sidebar-accent: oklch(0.20 0.15 315);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(0.22 0.12 315);
|
||||
--sidebar-ring: oklch(0.75 0.32 315);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
@layer utilities {
|
||||
.gradient-purple {
|
||||
background: linear-gradient(135deg,
|
||||
oklch(0.75 0.32 315) 0%,
|
||||
oklch(0.68 0.28 280) 30%,
|
||||
oklch(0.65 0.30 250) 70%,
|
||||
oklch(0.60 0.25 330) 100%);
|
||||
}
|
||||
|
||||
.gradient-purple-light {
|
||||
background: linear-gradient(135deg,
|
||||
oklch(0.95 0.12 315) 0%,
|
||||
oklch(0.96 0.10 280) 50%,
|
||||
oklch(0.98 0.08 250) 100%);
|
||||
}
|
||||
|
||||
.bg-gradient-smooth {
|
||||
background: linear-gradient(135deg,
|
||||
oklch(0.22 0.20 315) 0%,
|
||||
oklch(0.20 0.18 280) 30%,
|
||||
oklch(0.18 0.16 250) 60%,
|
||||
oklch(0.15 0.12 330) 100%);
|
||||
}
|
||||
|
||||
.text-gradient-bright {
|
||||
background: linear-gradient(135deg,
|
||||
oklch(0.85 0.35 315) 0%,
|
||||
oklch(0.80 0.32 280) 40%,
|
||||
oklch(0.75 0.30 250) 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
text-shadow: 0 0 20px oklch(0.75 0.32 315 / 0.4);
|
||||
}
|
||||
|
||||
.text-gradient {
|
||||
background: linear-gradient(135deg,
|
||||
oklch(0.75 0.32 315) 0%,
|
||||
oklch(0.70 0.30 280) 50%,
|
||||
oklch(0.68 0.28 250) 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
/* Glass Morphism Effects */
|
||||
.glass-card {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
box-shadow:
|
||||
0 8px 32px rgba(168, 85, 247, 0.18),
|
||||
0 4px 16px rgba(147, 51, 234, 0.12),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.glass-card:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
box-shadow:
|
||||
0 12px 40px rgba(168, 85, 247, 0.25),
|
||||
0 6px 20px rgba(147, 51, 234, 0.18),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.glass-input {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.glass-input:focus {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border: 1px solid rgba(168, 85, 247, 0.5);
|
||||
box-shadow:
|
||||
0 0 0 3px rgba(168, 85, 247, 0.15),
|
||||
0 4px 16px rgba(147, 51, 234, 0.25);
|
||||
}
|
||||
|
||||
.glass-button {
|
||||
background: linear-gradient(135deg,
|
||||
rgba(168, 85, 247, 0.9) 0%,
|
||||
rgba(120, 119, 248, 0.9) 40%,
|
||||
rgba(59, 130, 246, 0.85) 100%);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
box-shadow:
|
||||
0 8px 32px rgba(168, 85, 247, 0.35),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.glass-button::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg,
|
||||
transparent,
|
||||
rgba(255, 255, 255, 0.25),
|
||||
transparent);
|
||||
transition: left 0.5s ease;
|
||||
}
|
||||
|
||||
.glass-button:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.glass-button:hover {
|
||||
background: linear-gradient(135deg,
|
||||
rgba(168, 85, 247, 1) 0%,
|
||||
rgba(120, 119, 248, 1) 40%,
|
||||
rgba(59, 130, 246, 0.95) 100%);
|
||||
box-shadow:
|
||||
0 12px 40px rgba(168, 85, 247, 0.45),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.3);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.glass-button:active {
|
||||
transform: translateY(0);
|
||||
box-shadow:
|
||||
0 4px 16px rgba(168, 85, 247, 0.35),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.glass-secondary {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(16px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.glass-secondary:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||
box-shadow: 0 8px 24px rgba(139, 69, 199, 0.15);
|
||||
}
|
||||
|
||||
/* Обеспечиваем курсор pointer для всех кликабельных элементов */
|
||||
button, [role="button"], [data-state] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Специальные стили для вкладок */
|
||||
[data-slot="tabs-list"] {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
[data-slot="tabs-trigger"] {
|
||||
cursor: pointer !important;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
[data-slot="tabs-trigger"]:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
[data-slot="tabs-trigger"][data-state="active"] {
|
||||
background: rgba(255, 255, 255, 0.2) !important;
|
||||
color: white !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
/* Animated Background */
|
||||
.bg-animated {
|
||||
background: linear-gradient(135deg,
|
||||
oklch(0.22 0.20 315) 0%,
|
||||
oklch(0.18 0.16 300) 40%,
|
||||
oklch(0.15 0.12 330) 100%);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bg-animated::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background:
|
||||
radial-gradient(circle at 20% 50%, rgba(168, 85, 247, 0.35) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 20%, rgba(120, 119, 248, 0.35) 0%, transparent 50%),
|
||||
radial-gradient(circle at 40% 80%, rgba(59, 130, 246, 0.25) 0%, transparent 50%),
|
||||
radial-gradient(circle at 60% 30%, rgba(192, 132, 252, 0.20) 0%, transparent 50%);
|
||||
animation: float 20s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { opacity: 1; transform: translateY(0px) rotate(0deg); }
|
||||
33% { opacity: 0.8; transform: translateY(-20px) rotate(2deg); }
|
||||
66% { opacity: 0.9; transform: translateY(10px) rotate(-1deg); }
|
||||
}
|
||||
|
||||
/* Floating Particles Effect */
|
||||
.particles {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
overflow: hidden;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.particle {
|
||||
position: absolute;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
animation: particleFloat 15s linear infinite;
|
||||
}
|
||||
|
||||
.particle:nth-child(1) { width: 3px; height: 3px; left: 10%; animation-delay: 0s; }
|
||||
.particle:nth-child(2) { width: 2px; height: 2px; left: 20%; animation-delay: 2s; }
|
||||
.particle:nth-child(3) { width: 4px; height: 4px; left: 30%; animation-delay: 4s; }
|
||||
.particle:nth-child(4) { width: 2px; height: 2px; left: 40%; animation-delay: 6s; }
|
||||
.particle:nth-child(5) { width: 3px; height: 3px; left: 50%; animation-delay: 8s; }
|
||||
.particle:nth-child(6) { width: 2px; height: 2px; left: 60%; animation-delay: 10s; }
|
||||
.particle:nth-child(7) { width: 4px; height: 4px; left: 70%; animation-delay: 12s; }
|
||||
.particle:nth-child(8) { width: 2px; height: 2px; left: 80%; animation-delay: 14s; }
|
||||
.particle:nth-child(9) { width: 3px; height: 3px; left: 90%; animation-delay: 16s; }
|
||||
|
||||
@keyframes particleFloat {
|
||||
0% { transform: translateY(100vh) rotate(0deg); opacity: 0; }
|
||||
10% { opacity: 1; }
|
||||
90% { opacity: 1; }
|
||||
100% { transform: translateY(-100px) rotate(360deg); opacity: 0; }
|
||||
}
|
||||
|
||||
/* Enhanced Glow Effects */
|
||||
.glow-purple {
|
||||
box-shadow:
|
||||
0 0 20px rgba(168, 85, 247, 0.5),
|
||||
0 0 40px rgba(120, 119, 248, 0.35),
|
||||
0 0 60px rgba(59, 130, 246, 0.2),
|
||||
0 0 80px rgba(192, 132, 252, 0.15);
|
||||
}
|
||||
|
||||
.glow-text {
|
||||
text-shadow:
|
||||
0 0 10px rgba(168, 85, 247, 0.6),
|
||||
0 0 20px rgba(120, 119, 248, 0.45),
|
||||
0 0 30px rgba(59, 130, 246, 0.3),
|
||||
0 0 40px rgba(192, 132, 252, 0.25);
|
||||
}
|
||||
}
|
||||
|
@ -1,34 +1,21 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
"use client"
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
import { ApolloProvider } from '@apollo/client'
|
||||
import { apolloClient } from '@/lib/apollo-client'
|
||||
import "./globals.css"
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
<html lang="ru">
|
||||
<body>
|
||||
<ApolloProvider client={apolloClient}>
|
||||
{children}
|
||||
</ApolloProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
12
src/app/login/page.tsx
Normal file
12
src/app/login/page.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { AuthGuard } from "@/components/auth-guard"
|
||||
import { AuthFlow } from "@/components/auth/auth-flow"
|
||||
import { redirect } from "next/navigation"
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<AuthGuard fallback={<AuthFlow />}>
|
||||
{/* Если пользователь авторизован, перенаправляем в дашборд */}
|
||||
{redirect('/dashboard')}
|
||||
</AuthGuard>
|
||||
)
|
||||
}
|
10
src/app/market/page.tsx
Normal file
10
src/app/market/page.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { AuthGuard } from "@/components/auth-guard"
|
||||
import { MarketDashboard } from "@/components/market/market-dashboard"
|
||||
|
||||
export default function MarketPage() {
|
||||
return (
|
||||
<AuthGuard>
|
||||
<MarketDashboard />
|
||||
</AuthGuard>
|
||||
)
|
||||
}
|
117
src/app/page.tsx
117
src/app/page.tsx
@ -1,103 +1,24 @@
|
||||
import Image from "next/image";
|
||||
"use client"
|
||||
|
||||
import { useEffect } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useAuth } from "@/hooks/useAuth"
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
|
||||
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={180}
|
||||
height={38}
|
||||
priority
|
||||
/>
|
||||
<ol className="font-mono list-inside list-decimal text-sm/6 text-center sm:text-left">
|
||||
<li className="mb-2 tracking-[-.01em]">
|
||||
Get started by editing{" "}
|
||||
<code className="bg-black/[.05] dark:bg-white/[.06] font-mono font-semibold px-1 py-0.5 rounded">
|
||||
src/app/page.tsx
|
||||
</code>
|
||||
.
|
||||
</li>
|
||||
<li className="tracking-[-.01em]">
|
||||
Save and see your changes instantly.
|
||||
</li>
|
||||
</ol>
|
||||
const router = useRouter()
|
||||
const { user } = useAuth()
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
router.replace('/dashboard')
|
||||
} else {
|
||||
router.replace('/login')
|
||||
}
|
||||
}, [router, user])
|
||||
|
||||
<div className="flex gap-4 items-center flex-col sm:flex-row">
|
||||
<a
|
||||
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
Deploy now
|
||||
</a>
|
||||
<a
|
||||
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Read our docs
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/file.svg"
|
||||
alt="File icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Learn
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/window.svg"
|
||||
alt="Window icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Examples
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/globe.svg"
|
||||
alt="Globe icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Go to nextjs.org →
|
||||
</a>
|
||||
</footer>
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-smooth flex items-center justify-center">
|
||||
<div className="text-white">Загрузка...</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
27
src/app/register/page.tsx
Normal file
27
src/app/register/page.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
"use client"
|
||||
|
||||
import { Suspense } from 'react'
|
||||
import { AuthGuard } from "@/components/auth-guard"
|
||||
import { AuthFlow } from "@/components/auth/auth-flow"
|
||||
import { redirect } from "next/navigation"
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
|
||||
function RegisterContent() {
|
||||
const searchParams = useSearchParams()
|
||||
const partnerCode = searchParams.get('partner')
|
||||
|
||||
return (
|
||||
<AuthGuard fallback={<AuthFlow partnerCode={partnerCode} />}>
|
||||
{/* Если пользователь авторизован, перенаправляем в дашборд */}
|
||||
{redirect('/dashboard')}
|
||||
</AuthGuard>
|
||||
)
|
||||
}
|
||||
|
||||
export default function RegisterPage() {
|
||||
return (
|
||||
<Suspense fallback={<div>Загрузка...</div>}>
|
||||
<RegisterContent />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
10
src/app/settings/page.tsx
Normal file
10
src/app/settings/page.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { AuthGuard } from "@/components/auth-guard"
|
||||
import { UserSettings } from "@/components/dashboard/user-settings"
|
||||
|
||||
export default function SettingsPage() {
|
||||
return (
|
||||
<AuthGuard>
|
||||
<UserSettings />
|
||||
</AuthGuard>
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user