diff --git a/frontend/.env b/frontend/.env index a8cf54a..b8ca0df 100644 --- a/frontend/.env +++ b/frontend/.env @@ -1 +1 @@ -VITE_API_BASE_URL=http://localhost:8080 +VITE_API_BASE_URL= diff --git a/frontend/package-lock.json b/frontend/package-lock.json index cff8d8e..94e1ce8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@tailwindcss/postcss": "^4.1.18", "axios": "^1.13.2", + "lucide-react": "^0.561.0", "react": "^19.2.0", "react-dom": "^19.2.0", "react-router-dom": "^7.10.1", @@ -3436,6 +3437,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.561.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.561.0.tgz", + "integrity": "sha512-Y59gMY38tl4/i0qewcqohPdEbieBy7SovpBL9IFebhc2mDd8x4PZSOsiFRkpPcOq6bj1r/mjH/Rk73gSlIJP2A==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", diff --git a/frontend/package.json b/frontend/package.json index 8bc7062..80ee3b6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,6 +12,7 @@ "dependencies": { "@tailwindcss/postcss": "^4.1.18", "axios": "^1.13.2", + "lucide-react": "^0.561.0", "react": "^19.2.0", "react-dom": "^19.2.0", "react-router-dom": "^7.10.1", diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 31f1ae2..8a60fb5 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -13,7 +13,7 @@ import type { VerifyFamilyPasswordResponse, } from '../types'; -const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000'; +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || ''; const apiClient = axios.create({ baseURL: API_BASE_URL, diff --git a/frontend/src/index.css b/frontend/src/index.css index b5c61c9..56ffd94 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,3 +1,51 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; +@import "tailwindcss"; + +.gradient-bg { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); +} + +.gradient-bg-light { + background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); +} + +.card-hover { + transition: all 0.3s; +} + +.card-hover:hover { + box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); + transform: translateY(-0.25rem); +} + +.glass-effect { + background: rgb(255 255 255 / 0.8); + backdrop-filter: blur(12px); +} + +.animate-fadeIn { + animation: fadeIn 0.2s ease-in-out; +} + +.animate-scaleIn { + animation: scaleIn 0.2s ease-in-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes scaleIn { + from { + opacity: 0; + transform: scale(0.95); + } + to { + opacity: 1; + transform: scale(1); + } +} diff --git a/frontend/src/pages/AdminPanel.tsx b/frontend/src/pages/AdminPanel.tsx index 324604c..b7832cc 100644 --- a/frontend/src/pages/AdminPanel.tsx +++ b/frontend/src/pages/AdminPanel.tsx @@ -2,6 +2,18 @@ import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { authApi, familyApi } from '../api/client'; import { useStore } from '../store/useStore'; +import { + Shield, + Home, + LogOut, + Users, + Plus, + Trash2, + Lock, + User, + ArrowLeft, + X, +} from 'lucide-react'; export default function AdminPanel() { const navigate = useNavigate(); @@ -95,131 +107,203 @@ export default function AdminPanel() { if (!isAuthenticated) { return ( -
-
-

- Вход в админ панель -

- - {loginError && ( -
- {loginError} +
+
+
+
+
+ +
- )} +

+ Админ панель +

+

+ Войдите для управления системой +

-
-
- - setUsername(e.target.value)} - className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" - required - /> -
+ {loginError && ( +
+
+ + {loginError} +
+
+ )} -
- - setPassword(e.target.value)} - className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" - required - /> -
+ +
+ + setUsername(e.target.value)} + className="w-full px-4 py-2.5 sm:py-3 border-2 border-gray-300 rounded-xl focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-all text-sm sm:text-base" + placeholder="Введите логин" + required + /> +
+ +
+ + setPassword(e.target.value)} + className="w-full px-4 py-2.5 sm:py-3 border-2 border-gray-300 rounded-xl focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-all text-sm sm:text-base" + placeholder="Введите пароль" + required + /> +
+ + +
- - - +
); } return ( -
+
-
-

+
+
+ +
+

Админ панель

- -
- -
-

- Создать новую семью -

- -
- setNewFamilyName(e.target.value)} - className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" - /> - setNewFamilyPassword(e.target.value)} - className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" - /> +

+ Управление семьями +

+
+
-
-

- Список семей -

+
+
+
+ +
+

+ Создать новую семью +

+
+ +
+
+ + setNewFamilyName(e.target.value)} + className="w-full px-5 py-4 border-2 border-gray-300 rounded-2xl focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-all font-medium text-center" + /> +
+
+ + setNewFamilyPassword(e.target.value)} + className="w-full px-5 py-4 border-2 border-gray-300 rounded-2xl focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-all font-medium text-center" + /> +
+ +
+
+ +
+
+
+ +
+

+ Список семей +

+
{families.length === 0 ? ( -

- Семьи не найдены -

+
+
+ +
+

+ Семьи не найдены +

+

+ Создайте первую семью +

+
) : ( -
+
{families.map((family) => (
- - {family.name} - +
+
+ +
+ + {family.name} + +
@@ -227,13 +311,6 @@ export default function AdminPanel() {
)}
- -
); diff --git a/frontend/src/pages/FamilyView.tsx b/frontend/src/pages/FamilyView.tsx index 3fe3913..c687f1d 100644 --- a/frontend/src/pages/FamilyView.tsx +++ b/frontend/src/pages/FamilyView.tsx @@ -3,6 +3,18 @@ import { useParams, useNavigate } from 'react-router-dom'; import { categoryApi, expenseApi } from '../api/client'; import { useStore } from '../store/useStore'; import type { Category } from '../types'; +import { + ArrowLeft, + Wallet, + TrendingDown, + Plus, + Trash2, + RotateCcw, + Loader2, + X, + DollarSign, + Tag, +} from 'lucide-react'; export default function FamilyView() { const { familyId } = useParams<{ familyId: string }>(); @@ -140,160 +152,255 @@ export default function FamilyView() { if (loading) { return ( -
-
Загрузка...
+
+
+ + Загрузка... +
); } + const getProgressColor = (remaining: number, limit: number) => { + const percentage = (remaining / limit) * 100; + if (percentage > 50) return 'bg-green-500'; + if (percentage > 25) return 'bg-yellow-500'; + return 'bg-red-500'; + }; + + const getProgressPercentage = (remaining: number, limit: number) => { + return Math.max(0, Math.min(100, (remaining / limit) * 100)); + }; + return ( -
-
-
-
- -

+
+
+
+ +
+
+ +
+

{selectedFamily?.name || 'Семья'}

+

+ Управление категориями и расходами +

{error && ( -
- {error} +
+
+ + {error} +
)} -
- {categories.map((category) => ( -
-
-

- {category.name} -

-

- Остаток: - {remainingLimits.get(category.id)?.toFixed(2) || '0.00'} ₽ - - {' / '} - {category.limit_amount.toString()} ₽ -

-
+
+ {categories.map((category) => { + const remaining = remainingLimits.get(category.id) || 0; + const limit = parseFloat(category.limit_amount.toString()); + const percentage = getProgressPercentage(remaining, limit); -
- {showAddExpense === category.id ? ( -
- setExpenseAmount(e.target.value)} - className="w-full mb-2 px-3 py-2 border border-gray-300 rounded" + return ( +
+
+
+
+ +
+

+ {category.name} +

+
+ + {showAddExpense !== category.id && ( + + )} +
+ +
+
+ Остаток: + + {remaining.toFixed(2)} ₽ + +
+
+ Лимит: + {limit.toFixed(2)} ₽ +
+ +
+
- setExpenseDescription(e.target.value)} - className="w-full mb-2 px-3 py-2 border border-gray-300 rounded" - /> -
- - +
+

+ {percentage.toFixed(0)}% осталось +

+
+ + {showAddExpense === category.id && ( +
+

+ Добавить расход +

+
+
+ + setExpenseAmount(e.target.value)} + className="w-full px-4 py-3 border-2 border-gray-300 rounded-2xl focus:border-purple-500 focus:ring-2 focus:ring-purple-200 transition-all text-center font-semibold text-lg" + /> +
+
+ + setExpenseDescription(e.target.value)} + className="w-full px-4 py-3 border-2 border-gray-300 rounded-2xl focus:border-purple-500 focus:ring-2 focus:ring-purple-200 transition-all" + /> +
+
+ + +
- ) : ( - )}
-
- ))} + ); + })}
-
-

- Управление категориями -

+
+
+
+ +
+

+ Управление категориями +

+
{showAddCategory ? ( -
- setNewCategoryName(e.target.value)} - className="w-full mb-2 px-4 py-2 border border-gray-300 rounded-lg" - /> - setNewCategoryLimit(e.target.value)} - className="w-full mb-2 px-4 py-2 border border-gray-300 rounded-lg" - /> -
- - +
+

+ Новая категория +

+
+ setNewCategoryName(e.target.value)} + className="w-full px-5 py-4 border-2 border-gray-300 rounded-2xl focus:border-purple-500 focus:ring-2 focus:ring-purple-200 transition-all font-medium" + /> + setNewCategoryLimit(e.target.value)} + className="w-full px-5 py-4 border-2 border-gray-300 rounded-2xl focus:border-purple-500 focus:ring-2 focus:ring-purple-200 transition-all font-medium text-center" + /> +
+ + +
) : ( )} -
+
{categories.map((category) => ( -
- {category.name} +
+
+
+ +
+ + {category.name} + +
diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index c3a83bb..1292a6f 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom'; import { familyApi, categoryApi } from '../api/client'; import { useStore } from '../store/useStore'; import type { Family } from '../types'; +import { Users, Settings, Lock, Loader2, X } from 'lucide-react'; export default function Home() { const navigate = useNavigate(); @@ -88,95 +89,134 @@ export default function Home() { if (loading) { return ( -
-
Загрузка...
+
+
+ + Загрузка... +
); } return ( -
-
-
-

+
+
+
+

Семейный бюджет

{error && ( -
- {error} +
+
+ + {error} +
)} -
-

- Выберите семью -

- - {families.length === 0 ? ( -

- Семьи не найдены. Создайте семью в админ панели. -

- ) : ( -
- {families.map((family) => ( - - ))} +
+
+
+
- )} +

+ Выберите семью +

+
+ {families.length === 0 ? ( +
+
+ +
+

+ Семьи не найдены +

+

+ Создайте семью в админ панели +

+
+ ) : ( +
+ {families.map((family) => ( + + ))} +
+ )} + {showPasswordDialog && selectedFamilyForAuth && ( -
-
-

- Введите пароль для семьи -

-

- {selectedFamilyForAuth.name} -

+
+
+
+
+ +
+

+ Защищённая семья +

+

+ {selectedFamilyForAuth.name} +

+
{passwordError && ( -
- {passwordError} +
+
+ + {passwordError} +
)} setFamilyPassword(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleVerifyPassword()} - className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent mb-4" + className="w-full px-5 py-4 border-2 border-gray-300 rounded-2xl focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-all mb-6 text-base text-center font-medium" autoFocus /> -
+
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 37ae4d9..2869179 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -6,8 +6,16 @@ export default defineConfig({ server: { port: 5173, proxy: { - '/api': { - target: 'http://localhost:3000', + '/families': { + target: 'http://localhost:8080', + changeOrigin: true, + }, + '/login': { + target: 'http://localhost:8080', + changeOrigin: true, + }, + '/logout': { + target: 'http://localhost:8080', changeOrigin: true, } }