change frontend routing strategy to hash
This commit is contained in:
parent
07180ef301
commit
2a3c6df965
9 changed files with 36 additions and 16 deletions
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
1
docker/deployment.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
deployment is through the Dockerfile
|
||||||
|
|
@ -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} />
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue