Add backend API for personal finance management application
- Introduced a comprehensive backend API using TypeScript, Fastify, and PostgreSQL. - Added essential files including architecture documentation, environment configuration, and Docker setup. - Implemented RESTful routes for managing assets, liabilities, clients, invoices, and cashflow. - Established a robust database schema with Prisma for data management. - Integrated middleware for authentication and error handling. - Created service and repository layers to adhere to SOLID principles and clean architecture. - Included example environment variables for development, staging, and production setups.
This commit is contained in:
88
frontend-web/src/components/dialogs/LoginDialog.tsx
Normal file
88
frontend-web/src/components/dialogs/LoginDialog.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import {useState} from 'react';
|
||||
import {Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle} from '@/components/ui/dialog';
|
||||
import {Button} from '@/components/ui/button';
|
||||
import {Input} from '@/components/ui/input';
|
||||
import {Label} from '@/components/ui/label';
|
||||
import {useAppDispatch, setUser} from '@/store';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSwitchToSignUp: () => void;
|
||||
}
|
||||
|
||||
export default function LoginDialog({open, onOpenChange, onSwitchToSignUp}: Props) {
|
||||
const dispatch = useAppDispatch();
|
||||
const [form, setForm] = useState({
|
||||
email: '',
|
||||
password: '',
|
||||
});
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
|
||||
// Mock login - in production this would validate against an API
|
||||
if (!form.email || !form.password) {
|
||||
setError('Please enter your email and password');
|
||||
return;
|
||||
}
|
||||
|
||||
// Mock successful login
|
||||
dispatch(setUser({
|
||||
id: crypto.randomUUID(),
|
||||
email: form.email,
|
||||
name: form.email.split('@')[0],
|
||||
}));
|
||||
|
||||
onOpenChange(false);
|
||||
setForm({email: '', password: ''});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="card-elevated sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Welcome back</DialogTitle>
|
||||
<DialogDescription>Log in to your account</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="login-email">Email</Label>
|
||||
<Input
|
||||
id="login-email"
|
||||
type="email"
|
||||
placeholder="john@example.com"
|
||||
value={form.email}
|
||||
onChange={e => setForm({...form, email: e.target.value})}
|
||||
className="input-depth"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="login-password">Password</Label>
|
||||
<Input
|
||||
id="login-password"
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
value={form.password}
|
||||
onChange={e => setForm({...form, password: e.target.value})}
|
||||
className="input-depth"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{error && <p className="text-sm text-red-400">{error}</p>}
|
||||
</div>
|
||||
<DialogFooter className="flex-col gap-2 sm:flex-col">
|
||||
<Button type="submit" className="w-full">Log in</Button>
|
||||
<Button type="button" variant="ghost" className="w-full" onClick={onSwitchToSignUp}>
|
||||
Don't have an account? Sign up
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
122
frontend-web/src/components/dialogs/SignUpDialog.tsx
Normal file
122
frontend-web/src/components/dialogs/SignUpDialog.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import {useState} from 'react';
|
||||
import {Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle} from '@/components/ui/dialog';
|
||||
import {Button} from '@/components/ui/button';
|
||||
import {Input} from '@/components/ui/input';
|
||||
import {Label} from '@/components/ui/label';
|
||||
import {useAppDispatch, setUser} from '@/store';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSwitchToLogin: () => void;
|
||||
}
|
||||
|
||||
export default function SignUpDialog({open, onOpenChange, onSwitchToLogin}: Props) {
|
||||
const dispatch = useAppDispatch();
|
||||
const [form, setForm] = useState({
|
||||
name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
});
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
|
||||
if (form.password !== form.confirmPassword) {
|
||||
setError('Passwords do not match');
|
||||
return;
|
||||
}
|
||||
|
||||
if (form.password.length < 6) {
|
||||
setError('Password must be at least 6 characters');
|
||||
return;
|
||||
}
|
||||
|
||||
// Mock sign up - in production this would call an API
|
||||
dispatch(setUser({
|
||||
id: crypto.randomUUID(),
|
||||
email: form.email,
|
||||
name: form.name,
|
||||
}));
|
||||
|
||||
onOpenChange(false);
|
||||
setForm({name: '', email: '', password: '', confirmPassword: ''});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="card-elevated sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create an account</DialogTitle>
|
||||
<DialogDescription>Enter your details to get started</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="signup-name">Name</Label>
|
||||
<Input
|
||||
id="signup-name"
|
||||
type="text"
|
||||
placeholder="John Doe"
|
||||
value={form.name}
|
||||
onChange={e => setForm({...form, name: e.target.value})}
|
||||
className="input-depth"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="signup-email">Email</Label>
|
||||
<Input
|
||||
id="signup-email"
|
||||
type="email"
|
||||
placeholder="john@example.com"
|
||||
value={form.email}
|
||||
onChange={e => setForm({...form, email: e.target.value})}
|
||||
className="input-depth"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="signup-password">Password</Label>
|
||||
<Input
|
||||
id="signup-password"
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
value={form.password}
|
||||
onChange={e => setForm({...form, password: e.target.value})}
|
||||
className="input-depth"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="signup-confirm">Confirm Password</Label>
|
||||
<Input
|
||||
id="signup-confirm"
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
value={form.confirmPassword}
|
||||
onChange={e => setForm({...form, confirmPassword: e.target.value})}
|
||||
className="input-depth"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{error && <p className="text-sm text-red-400">{error}</p>}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
By signing up, you agree to our Terms of Service and Privacy Policy.
|
||||
</p>
|
||||
</div>
|
||||
<DialogFooter className="flex-col gap-2 sm:flex-col">
|
||||
<Button type="submit" className="w-full">Create account</Button>
|
||||
<Button type="button" variant="ghost" className="w-full" onClick={onSwitchToLogin}>
|
||||
Already have an account? Log in
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user