Create new Flutter app
The prompt requests the creation of a new app with a neumorphic design, similar to a household account book, using Flutter.
This commit is contained in:
45
src/components/AddTransactionButton.tsx
Normal file
45
src/components/AddTransactionButton.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { PlusIcon, MinusIcon, X } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const AddTransactionButton = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-24 right-6 z-20">
|
||||
{isOpen && (
|
||||
<div className="absolute bottom-16 right-0 flex flex-col gap-3 items-end animate-slide-up">
|
||||
<button
|
||||
className="neuro-flat p-4 text-neuro-expense flex items-center gap-2"
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
<span className="text-sm font-medium">지출</span>
|
||||
<MinusIcon size={18} />
|
||||
</button>
|
||||
<button
|
||||
className="neuro-flat p-4 text-neuro-income flex items-center gap-2"
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
<span className="text-sm font-medium">수입</span>
|
||||
<PlusIcon size={18} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
className={cn(
|
||||
"p-4 rounded-full transition-all duration-300 text-white",
|
||||
isOpen
|
||||
? "bg-red-500 rotate-45 shadow-lg"
|
||||
: "bg-neuro-accent shadow-neuro-flat hover:shadow-neuro-convex animate-pulse-subtle"
|
||||
)}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
{isOpen ? <X size={24} /> : <PlusIcon size={24} />}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddTransactionButton;
|
||||
60
src/components/BudgetCard.tsx
Normal file
60
src/components/BudgetCard.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
|
||||
import React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface BudgetCardProps {
|
||||
title: string;
|
||||
current: number;
|
||||
total: number;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
const BudgetCard: React.FC<BudgetCardProps> = ({
|
||||
title,
|
||||
current,
|
||||
total,
|
||||
color = 'neuro-accent'
|
||||
}) => {
|
||||
const percentage = Math.min(Math.round((current / total) * 100), 100);
|
||||
|
||||
const formattedCurrent = new Intl.NumberFormat('ko-KR', {
|
||||
style: 'currency',
|
||||
currency: 'KRW',
|
||||
maximumFractionDigits: 0
|
||||
}).format(current);
|
||||
|
||||
const formattedTotal = new Intl.NumberFormat('ko-KR', {
|
||||
style: 'currency',
|
||||
currency: 'KRW',
|
||||
maximumFractionDigits: 0
|
||||
}).format(total);
|
||||
|
||||
return (
|
||||
<div className="neuro-card">
|
||||
<h3 className="text-sm font-medium text-gray-600 mb-1">{title}</h3>
|
||||
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<p className="text-lg font-semibold">{formattedCurrent}</p>
|
||||
<p className="text-sm text-gray-500">/ {formattedTotal}</p>
|
||||
</div>
|
||||
|
||||
<div className="relative h-3 neuro-pressed overflow-hidden">
|
||||
<div
|
||||
className={`absolute top-0 left-0 h-full transition-all duration-700 ease-out bg-${color}`}
|
||||
style={{ width: `${percentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 flex justify-end">
|
||||
<span className={cn(
|
||||
"text-xs font-medium",
|
||||
percentage >= 90 ? "text-neuro-expense" : "text-gray-500"
|
||||
)}>
|
||||
{percentage}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BudgetCard;
|
||||
43
src/components/ExpenseChart.tsx
Normal file
43
src/components/ExpenseChart.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
|
||||
import React from 'react';
|
||||
import { PieChart, Pie, Cell, ResponsiveContainer, Legend } from 'recharts';
|
||||
|
||||
interface ExpenseData {
|
||||
name: string;
|
||||
value: number;
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface ExpenseChartProps {
|
||||
data: ExpenseData[];
|
||||
}
|
||||
|
||||
const ExpenseChart: React.FC<ExpenseChartProps> = ({ data }) => {
|
||||
return (
|
||||
<div className="neuro-card h-64">
|
||||
<h3 className="text-sm font-medium text-gray-600 mb-3">카테고리별 지출</h3>
|
||||
|
||||
<ResponsiveContainer width="100%" height="85%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={data}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={50}
|
||||
outerRadius={70}
|
||||
paddingAngle={5}
|
||||
dataKey="value"
|
||||
labelLine={false}
|
||||
label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
|
||||
>
|
||||
{data.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExpenseChart;
|
||||
49
src/components/NavBar.tsx
Normal file
49
src/components/NavBar.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
|
||||
import React from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { Home, BarChart2, Calendar, Settings } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const NavBar = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const navItems = [
|
||||
{ icon: Home, label: '홈', path: '/' },
|
||||
{ icon: Calendar, label: '거래', path: '/transactions' },
|
||||
{ icon: BarChart2, label: '분석', path: '/analytics' },
|
||||
{ icon: Settings, label: '설정', path: '/settings' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-0 left-0 right-0 p-4 z-10 animate-slide-up">
|
||||
<div className="neuro-flat mx-auto max-w-md flex justify-around items-center py-3 px-6">
|
||||
{navItems.map((item) => {
|
||||
const isActive = location.pathname === item.path;
|
||||
return (
|
||||
<button
|
||||
key={item.path}
|
||||
onClick={() => navigate(item.path)}
|
||||
className={cn(
|
||||
"flex flex-col items-center gap-1 p-2 rounded-lg transition-all duration-300",
|
||||
isActive ? "text-neuro-accent" : "text-gray-500"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"p-2 rounded-full transition-all duration-300",
|
||||
isActive ? "neuro-pressed" : "neuro-flat"
|
||||
)}
|
||||
>
|
||||
<item.icon size={20} />
|
||||
</div>
|
||||
<span className="text-xs font-medium">{item.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavBar;
|
||||
72
src/components/TransactionCard.tsx
Normal file
72
src/components/TransactionCard.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
|
||||
import React from 'react';
|
||||
import { ArrowDownIcon, ArrowUpIcon, ShoppingBag, Coffee, Home, Car, Gift } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export type Transaction = {
|
||||
id: string;
|
||||
title: string;
|
||||
amount: number;
|
||||
date: string;
|
||||
category: string;
|
||||
type: 'expense' | 'income';
|
||||
};
|
||||
|
||||
const categoryIcons: Record<string, React.ReactNode> = {
|
||||
shopping: <ShoppingBag size={18} />,
|
||||
food: <Coffee size={18} />,
|
||||
housing: <Home size={18} />,
|
||||
transportation: <Car size={18} />,
|
||||
entertainment: <Gift size={18} />,
|
||||
// Add more categories as needed
|
||||
};
|
||||
|
||||
interface TransactionCardProps {
|
||||
transaction: Transaction;
|
||||
}
|
||||
|
||||
const TransactionCard: React.FC<TransactionCardProps> = ({ transaction }) => {
|
||||
const { title, amount, date, category, type } = transaction;
|
||||
|
||||
const formattedAmount = new Intl.NumberFormat('ko-KR', {
|
||||
style: 'currency',
|
||||
currency: 'KRW',
|
||||
maximumFractionDigits: 0
|
||||
}).format(amount);
|
||||
|
||||
return (
|
||||
<div className="neuro-flat p-4 transition-all duration-300 hover:shadow-neuro-convex animate-scale-in">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={cn(
|
||||
"p-2 rounded-full",
|
||||
type === 'expense' ? "bg-neuro-expense/10 text-neuro-expense" : "bg-neuro-income/10 text-neuro-income"
|
||||
)}>
|
||||
{categoryIcons[category] || <ShoppingBag size={18} />}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-sm font-medium">{title}</h3>
|
||||
<p className="text-xs text-gray-500">{date}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
{type === 'expense' ? (
|
||||
<ArrowDownIcon size={12} className="text-neuro-expense" />
|
||||
) : (
|
||||
<ArrowUpIcon size={12} className="text-neuro-income" />
|
||||
)}
|
||||
<span className={cn(
|
||||
"font-medium",
|
||||
type === 'expense' ? "text-neuro-expense" : "text-neuro-income"
|
||||
)}>
|
||||
{formattedAmount}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TransactionCard;
|
||||
Reference in New Issue
Block a user