feat: initial commit
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
import { useState } from 'react'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { Plus, Wallet } from 'lucide-react'
|
||||
import { getPiggyBanks, getCategories, createCategory, createPiggyBank, createTransaction, deletePiggyBank } from './lib/api'
|
||||
import type { PiggyBank } from './types'
|
||||
import { Button } from './components/ui/button'
|
||||
import { Dialog } from './components/ui/dialog'
|
||||
import { Input } from './components/ui/input'
|
||||
import { PiggyBankCard } from './components/PiggyBankCard'
|
||||
import { PiggyBankForm } from './components/PiggyBankForm'
|
||||
import { AddMoneyDialog } from './components/AddMoneyDialog'
|
||||
import { TransactionHistory } from './components/TransactionHistory'
|
||||
|
||||
export default function App() {
|
||||
const queryClient = useQueryClient()
|
||||
const [showForm, setShowForm] = useState(false)
|
||||
const [showCatForm, setShowCatForm] = useState(false)
|
||||
const [catName, setCatName] = useState('')
|
||||
const [selectedBank, setSelectedBank] = useState<PiggyBank | null>(null)
|
||||
|
||||
const { data: banks = [] } = useQuery({
|
||||
queryKey: ['piggy-banks'],
|
||||
queryFn: getPiggyBanks,
|
||||
})
|
||||
|
||||
const { data: categories = [] } = useQuery({
|
||||
queryKey: ['categories'],
|
||||
queryFn: getCategories,
|
||||
})
|
||||
|
||||
const createBank = useMutation({
|
||||
mutationFn: createPiggyBank,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['piggy-banks'] })
|
||||
setShowForm(false)
|
||||
},
|
||||
})
|
||||
|
||||
const addCategory = useMutation({
|
||||
mutationFn: (name: string) => createCategory(name),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['categories'] })
|
||||
setShowCatForm(false)
|
||||
setCatName('')
|
||||
},
|
||||
})
|
||||
|
||||
const addMoney = useMutation({
|
||||
mutationFn: ({ bankId, amount, description }: { bankId: number; amount: number; description?: string }) =>
|
||||
createTransaction(bankId, { amount, description }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['piggy-banks'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['transactions'] })
|
||||
},
|
||||
})
|
||||
|
||||
const removeBank = useMutation({
|
||||
mutationFn: deletePiggyBank,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['piggy-banks'] })
|
||||
},
|
||||
})
|
||||
|
||||
const totalSaved = banks.reduce((sum, b) => sum + b.current_amount, 0)
|
||||
const totalTarget = banks.reduce((sum, b) => sum + b.target_amount, 0)
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="mx-auto max-w-4xl px-4 py-8">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-blue-600">
|
||||
<Wallet className="h-6 w-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">Копилка</h1>
|
||||
<p className="text-sm text-gray-500">Сервис для накоплений</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="secondary" size="sm" onClick={() => setShowCatForm(true)}>
|
||||
+ Категория
|
||||
</Button>
|
||||
<Button size="sm" onClick={() => setShowForm(true)}>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
Копилка
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary */}
|
||||
{banks.length > 0 && (
|
||||
<div className="mb-6 grid grid-cols-3 gap-4">
|
||||
<div className="rounded-xl border border-gray-200 bg-white p-4">
|
||||
<p className="text-sm text-gray-500">Копилок</p>
|
||||
<p className="text-2xl font-bold text-gray-900">{banks.length}</p>
|
||||
</div>
|
||||
<div className="rounded-xl border border-gray-200 bg-white p-4">
|
||||
<p className="text-sm text-gray-500">Накоплено</p>
|
||||
<p className="text-2xl font-bold text-green-600">${totalSaved.toFixed(2)}</p>
|
||||
</div>
|
||||
<div className="rounded-xl border border-gray-200 bg-white p-4">
|
||||
<p className="text-sm text-gray-500">Цель</p>
|
||||
<p className="text-2xl font-bold text-blue-600">${totalTarget.toFixed(2)}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Piggy Banks Grid */}
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{banks.map((bank) => (
|
||||
<div key={bank.id} className="space-y-3">
|
||||
<PiggyBankCard
|
||||
bank={bank}
|
||||
categories={categories}
|
||||
onAddMoney={setSelectedBank}
|
||||
onDelete={(id) => removeBank.mutate(id)}
|
||||
/>
|
||||
<TransactionHistory bank={bank} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{banks.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center py-20 text-center">
|
||||
<Wallet className="h-16 w-16 text-gray-300 mb-4" />
|
||||
<h2 className="text-lg font-semibold text-gray-600 mb-2">
|
||||
Ещё нет ни одной копилки
|
||||
</h2>
|
||||
<p className="text-sm text-gray-400 mb-6">
|
||||
Создайте первую копилку и начните копить!
|
||||
</p>
|
||||
<Button onClick={() => setShowForm(true)}>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
Создать копилку
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Create Piggy Bank Dialog */}
|
||||
<Dialog open={showForm} onOpenChange={setShowForm} title="Новая копилка">
|
||||
<PiggyBankForm
|
||||
categories={categories}
|
||||
onSubmit={(data) => createBank.mutateAsync(data)}
|
||||
onCancel={() => setShowForm(false)}
|
||||
/>
|
||||
</Dialog>
|
||||
|
||||
{/* Add Money Dialog */}
|
||||
<AddMoneyDialog
|
||||
bank={selectedBank}
|
||||
open={!!selectedBank}
|
||||
onOpenChange={(open) => { if (!open) setSelectedBank(null) }}
|
||||
onSubmit={(bankId, amount, description) =>
|
||||
addMoney.mutateAsync({ bankId, amount, description })
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Create Category Dialog */}
|
||||
<Dialog open={showCatForm} onOpenChange={setShowCatForm} title="Новая категория">
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
if (catName.trim()) addCategory.mutate(catName.trim())
|
||||
}}
|
||||
className="space-y-4"
|
||||
>
|
||||
<Input
|
||||
label="Название категории"
|
||||
id="catName"
|
||||
placeholder="Например: Путешествия"
|
||||
value={catName}
|
||||
onChange={(e) => setCatName(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<div className="flex justify-end gap-3 pt-2">
|
||||
<Button type="button" variant="secondary" onClick={() => setShowCatForm(false)}>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button type="submit" disabled={!catName.trim()}>
|
||||
Создать
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user