change frontend routing strategy to hash

This commit is contained in:
david 2026-08-15 15:30:50 +03:00
parent 07180ef301
commit 2a3c6df965
9 changed files with 36 additions and 16 deletions

View file

@ -28,7 +28,7 @@ class Board(db.Model, SoftDeleteMixin):
) )
# Timestamps # Timestamps
date_last_activity = db.Column(db.DateTime) date_last_activity = db.Column(db.DateTime, default=lambda: datetime.now(UTC))
date_last_view = db.Column(db.DateTime) date_last_view = db.Column(db.DateTime)
created_at = db.Column(db.DateTime, default=lambda: datetime.now(UTC)) created_at = db.Column(db.DateTime, default=lambda: datetime.now(UTC))
updated_at = db.Column( updated_at = db.Column(

View file

@ -38,7 +38,7 @@ class Card(db.Model, SoftDeleteMixin):
) )
# Timestamps # Timestamps
date_last_activity = db.Column(db.DateTime) date_last_activity = db.Column(db.DateTime, default=lambda: datetime.now(UTC))
created_at = db.Column(db.DateTime, default=lambda: datetime.now(UTC)) created_at = db.Column(db.DateTime, default=lambda: datetime.now(UTC))
updated_at = db.Column( updated_at = db.Column(
db.DateTime, db.DateTime,

View file

@ -1,6 +1,19 @@
"""Serves the built React SPA.
With hash-based routing, client-side routes never reach the server
(e.g. /#/boards/123 arrives as GET /). The only paths ever requested are:
1. "/" -> the SPA entry point (index.html)
2. "/<static>" -> real files in the built static/ folder (JS, CSS, icons)
Everything else is a scanner probe (e.g. /.env, /cgi-bin/status,
/xmlrpc.php) and correctly gets 404. There is deliberately NO catch-all
that returns index.html for arbitrary paths.
"""
import os import os
from flask import Blueprint from flask import Blueprint, abort
from flask import current_app as app from flask import current_app as app
from flask import send_from_directory from flask import send_from_directory
@ -13,7 +26,7 @@ def serve_root():
@home_bp.route("/<path:path>") @home_bp.route("/<path:path>")
def serve_spa(path): def serve_static(path):
if os.path.exists(os.path.join(app.static_folder, path)): if os.path.exists(os.path.join(app.static_folder, path)):
return send_from_directory(app.static_folder, path) return send_from_directory(app.static_folder, path)
return send_from_directory(app.static_folder, "index.html") return abort(404)

1
docker/deployment.md Normal file
View file

@ -0,0 +1 @@
deployment is through the Dockerfile

View file

@ -7,7 +7,7 @@ export const BoardDetailLayout = ({ children }: { children: ReactNode }) => {
return ( return (
<div className="relative"> <div className="relative">
<div className="pr-6">{children}</div> <div className="md:pr-6">{children}</div>
{id && ( {id && (
<div className=""> <div className="">
<BoardSidebar boardId={id} /> <BoardSidebar boardId={id} />

View file

@ -68,7 +68,7 @@ export function KanbanColumn({
}; };
return ( return (
<div className="bg-gray-800 rounded-lg py-4 min-w-[300px] max-w-[300px] border border-gray-700 flex flex-col max-h-[calc(100vh-280px)] pr-2"> <div className="bg-gray-800 rounded-lg py-4 min-w-[300px] max-w-[300px] border border-gray-700 flex flex-col max-h-[calc(100vh-292px)] pr-2">
<div className="mb-4 px-4"> <div className="mb-4 px-4">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2 flex-1"> <div className="flex items-center gap-2 flex-1">

View file

@ -32,6 +32,13 @@ api.interceptors.request.use(
(error) => Promise.reject(error) (error) => Promise.reject(error)
); );
// Extract the current route path from the hash (e.g. "#/boards/123" -> "/boards/123").
// With HashRouter the pathname is always "/", so the real route lives in the fragment.
const getRoutePath = () => {
const hash = window.location.hash;
return hash ? hash.replace(/^#/, '') : '/';
};
// Handle response errors // Handle response errors
api.interceptors.response.use( api.interceptors.response.use(
(response) => response, (response) => response,
@ -41,9 +48,10 @@ api.interceptors.response.use(
localStorage.removeItem('token'); localStorage.removeItem('token');
localStorage.removeItem('user'); localStorage.removeItem('user');
if (!['/login', '/register'].includes(window.location.pathname)) { const currentPath = getRoutePath();
const currentPath = window.location.pathname; if (!['/login', '/register'].includes(currentPath)) {
window.location.href = `/login?redirect=${encodeURIComponent(currentPath)}`; // Hash-router login URL: the whole route + query string lives inside the hash
window.location.href = `#/login?redirect=${encodeURIComponent(currentPath)}`;
} }
} }
return Promise.reject(error); return Promise.reject(error);

View file

@ -1,16 +1,16 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom'; import { HashRouter } from 'react-router-dom';
import { AppProvider } from './context/AppContext'; import { AppProvider } from './context/AppContext';
import App from './App.tsx'; import App from './App.tsx';
import './index.css'; import './index.css';
ReactDOM.createRoot(document.getElementById('root')!).render( ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode> <React.StrictMode>
<BrowserRouter> <HashRouter>
<AppProvider> <AppProvider>
<App /> <App />
</AppProvider> </AppProvider>
</BrowserRouter> </HashRouter>
</React.StrictMode> </React.StrictMode>
); );

View file

@ -245,9 +245,8 @@ export function BoardDetail() {
</div> </div>
</WidePageLayout> </WidePageLayout>
<div className="px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-start gap-6"> <div className="flex justify-between items-start gap-6">
<div className="flex-1"> <div className="flex-1 overflow-x-auto">
<DndContext <DndContext
sensors={sensors} sensors={sensors}
collisionDetection={closestCenter} collisionDetection={closestCenter}
@ -291,7 +290,6 @@ export function BoardDetail() {
</DndContext> </DndContext>
</div> </div>
</div> </div>
</div>
</div> </div>
); );
} }