Compare commits
5 Commits
feat/simpl
...
e97203c4ab
| Author | SHA1 | Date | |
|---|---|---|---|
| e97203c4ab | |||
|
|
6dd8c01f5e | ||
| 495c1e89bb | |||
|
|
032a9f4e3b | ||
| d5f49fd86f |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@family-budget/backend",
|
"name": "@family-budget/backend",
|
||||||
"version": "0.1.0",
|
"version": "0.5.12",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tsx watch src/app.ts",
|
"dev": "tsx watch src/app.ts",
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import cookieParser from 'cookie-parser';
|
import cookieParser from 'cookie-parser';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
@@ -5,6 +7,10 @@ import { config } from './config';
|
|||||||
import { runMigrations } from './db/migrate';
|
import { runMigrations } from './db/migrate';
|
||||||
import { requireAuth } from './middleware/auth';
|
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 authRouter from './routes/auth';
|
||||||
import importRouter from './routes/import';
|
import importRouter from './routes/import';
|
||||||
import importsRouter from './routes/imports';
|
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)
|
// Auth routes (login is public; me/logout apply auth internally)
|
||||||
app.use('/api/auth', authRouter);
|
app.use('/api/auth', authRouter);
|
||||||
|
|
||||||
|
app.get('/api/version', (_req, res) => {
|
||||||
|
res.json({ version: pkg.version });
|
||||||
|
});
|
||||||
|
|
||||||
// All remaining /api routes require authentication
|
// All remaining /api routes require authentication
|
||||||
app.use('/api', requireAuth);
|
app.use('/api', requireAuth);
|
||||||
app.use('/api/import', importRouter);
|
app.use('/api/import', importRouter);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@family-budget/frontend",
|
"name": "@family-budget/frontend",
|
||||||
"version": "0.1.0",
|
"version": "0.8.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
5
frontend/src/api/version.ts
Normal file
5
frontend/src/api/version.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { api } from './client';
|
||||||
|
|
||||||
|
export async function getBackendVersion(): Promise<{ version: string }> {
|
||||||
|
return api.get<{ version: string }>('/api/version');
|
||||||
|
}
|
||||||
@@ -1,13 +1,21 @@
|
|||||||
import { useState, type ReactNode } from 'react';
|
import { useState, useEffect, type ReactNode } from 'react';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../context/AuthContext';
|
||||||
|
import { getBackendVersion } from '../api/version';
|
||||||
|
|
||||||
export function Layout({ children }: { children: ReactNode }) {
|
export function Layout({ children }: { children: ReactNode }) {
|
||||||
const { user, logout } = useAuth();
|
const { user, logout } = useAuth();
|
||||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
|
const [beVersion, setBeVersion] = useState<string | null>(null);
|
||||||
|
|
||||||
const closeDrawer = () => setDrawerOpen(false);
|
const closeDrawer = () => setDrawerOpen(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getBackendVersion()
|
||||||
|
.then((r) => setBeVersion(r.version))
|
||||||
|
.catch(() => setBeVersion(null));
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="layout">
|
<div className="layout">
|
||||||
<button
|
<button
|
||||||
@@ -86,11 +94,19 @@ export function Layout({ children }: { children: ReactNode }) {
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="sidebar-footer">
|
<div className="sidebar-footer">
|
||||||
|
<div className="sidebar-footer-top">
|
||||||
<span className="sidebar-user">{user?.login}</span>
|
<span className="sidebar-user">{user?.login}</span>
|
||||||
<button className="btn-logout" onClick={() => logout()}>
|
<button className="btn-logout" onClick={() => logout()}>
|
||||||
Выход
|
Выход
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
</aside>
|
||||||
|
|
||||||
<main className="main-content">{children}</main>
|
<main className="main-content">{children}</main>
|
||||||
|
|||||||
@@ -138,11 +138,34 @@ body {
|
|||||||
.sidebar-footer {
|
.sidebar-footer {
|
||||||
padding: 16px 20px;
|
padding: 16px 20px;
|
||||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-footer-top {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
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 {
|
.sidebar-user {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--color-sidebar-text);
|
color: var(--color-sidebar-text);
|
||||||
@@ -249,6 +272,7 @@ body {
|
|||||||
|
|
||||||
.page {
|
.page {
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header {
|
.page-header {
|
||||||
|
|||||||
2
frontend/src/vite-env.d.ts
vendored
2
frontend/src/vite-env.d.ts
vendored
@@ -1 +1,3 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
declare const __FE_VERSION__: string;
|
||||||
|
|||||||
@@ -1,8 +1,19 @@
|
|||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
import react from '@vitejs/plugin-react';
|
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({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
|
define: {
|
||||||
|
__FE_VERSION__: JSON.stringify(pkg.version),
|
||||||
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 5173,
|
port: 5173,
|
||||||
proxy: {
|
proxy: {
|
||||||
|
|||||||
Reference in New Issue
Block a user