9 Commits

11 changed files with 81 additions and 28 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "@family-budget/backend",
"version": "0.1.0",
"version": "0.5.12",
"private": true,
"scripts": {
"dev": "tsx watch src/app.ts",

View File

@@ -1,3 +1,5 @@
import fs from 'fs';
import path from 'path';
import express from 'express';
import cookieParser from 'cookie-parser';
import cors from 'cors';
@@ -5,6 +7,10 @@ import { config } from './config';
import { runMigrations } from './db/migrate';
import { requireAuth } from './middleware/auth';
const pkg = JSON.parse(
fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'),
) as { version: string };
import authRouter from './routes/auth';
import importRouter from './routes/import';
import importsRouter from './routes/imports';
@@ -24,6 +30,10 @@ app.use(cors({ origin: true, credentials: true }));
// Auth routes (login is public; me/logout apply auth internally)
app.use('/api/auth', authRouter);
app.get('/api/version', (_req, res) => {
res.json({ version: pkg.version });
});
// All remaining /api routes require authentication
app.use('/api', requireAuth);
app.use('/api/import', importRouter);

View File

@@ -4,6 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#0f172a" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<title>Семейный бюджет</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />

View File

@@ -1,6 +1,6 @@
{
"name": "@family-budget/frontend",
"version": "0.1.0",
"version": "0.8.5",
"private": true,
"type": "module",
"scripts": {

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<rect width="32" height="32" rx="8" fill="#0f172a"/>
<text x="16" y="23" font-family="Georgia, serif" font-size="20" font-weight="bold" fill="#3b82f6" text-anchor="middle">&#8381;</text>
</svg>

After

Width:  |  Height:  |  Size: 271 B

View File

@@ -0,0 +1,5 @@
import { api } from './client';
export async function getBackendVersion(): Promise<{ version: string }> {
return api.get<{ version: string }>('/api/version');
}

View File

@@ -1,8 +1,6 @@
import { useState } from 'react';
import { clearAllTransactions } from '../api/transactions';
const CONFIRM_WORD = 'УДАЛИТЬ';
interface Props {
onClose: () => void;
onDone: () => void;
@@ -10,15 +8,11 @@ interface Props {
export function ClearHistoryModal({ onClose, onDone }: Props) {
const [check1, setCheck1] = useState(false);
const [confirmInput, setConfirmInput] = useState('');
const [check2, setCheck2] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const canConfirm =
check1 &&
confirmInput.trim().toUpperCase() === CONFIRM_WORD &&
check2;
const canConfirm = check1 && check2;
const handleConfirm = async () => {
if (!canConfirm || loading) return;
@@ -65,20 +59,6 @@ export function ClearHistoryModal({ onClose, onDone }: Props) {
</label>
</div>
<div className="form-group">
<label>
Введите <strong>{CONFIRM_WORD}</strong> для подтверждения
</label>
<input
type="text"
value={confirmInput}
onChange={(e) => setConfirmInput(e.target.value)}
placeholder={CONFIRM_WORD}
className={confirmInput && confirmInput.trim().toUpperCase() !== CONFIRM_WORD ? 'input-error' : ''}
autoComplete="off"
/>
</div>
<div className="form-group form-group-checkbox clear-history-check">
<label>
<input

View File

@@ -1,13 +1,21 @@
import { useState, type ReactNode } from 'react';
import { useState, useEffect, type ReactNode } from 'react';
import { NavLink } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import { getBackendVersion } from '../api/version';
export function Layout({ children }: { children: ReactNode }) {
const { user, logout } = useAuth();
const [drawerOpen, setDrawerOpen] = useState(false);
const [beVersion, setBeVersion] = useState<string | null>(null);
const closeDrawer = () => setDrawerOpen(false);
useEffect(() => {
getBackendVersion()
.then((r) => setBeVersion(r.version))
.catch(() => setBeVersion(null));
}, []);
return (
<div className="layout">
<button
@@ -86,11 +94,19 @@ export function Layout({ children }: { children: ReactNode }) {
</nav>
<div className="sidebar-footer">
<div className="sidebar-footer-top">
<span className="sidebar-user">{user?.login}</span>
<button className="btn-logout" onClick={() => logout()}>
Выход
</button>
</div>
<div className="sidebar-footer-bottom">
<span className="sidebar-version">
FE {__FE_VERSION__} · BE {beVersion ?? '…'}
</span>
<span className="sidebar-copyright">© 2025 Семейный бюджет</span>
</div>
</div>
</aside>
<main className="main-content">{children}</main>

View File

@@ -138,11 +138,34 @@ body {
.sidebar-footer {
padding: 16px 20px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
flex-direction: column;
gap: 10px;
}
.sidebar-footer-top {
display: flex;
align-items: center;
justify-content: space-between;
}
.sidebar-footer-bottom {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 11px;
color: var(--color-sidebar-text);
opacity: 0.85;
}
.sidebar-version {
font-family: ui-monospace, monospace;
}
.sidebar-copyright {
font-size: 10px;
}
.sidebar-user {
font-size: 13px;
color: var(--color-sidebar-text);
@@ -249,6 +272,7 @@ body {
.page {
max-width: 1200px;
margin: 0 auto;
}
.page-header {

View File

@@ -1 +1,3 @@
/// <reference types="vite/client" />
declare const __FE_VERSION__: string;

View File

@@ -1,8 +1,19 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { readFileSync } from 'fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const pkg = JSON.parse(
readFileSync(join(__dirname, 'package.json'), 'utf-8'),
) as { version: string };
export default defineConfig({
plugins: [react()],
define: {
__FE_VERSION__: JSON.stringify(pkg.version),
},
server: {
port: 5173,
proxy: {