base inertia setup

This commit is contained in:
LeNei
2025-03-28 10:00:09 +01:00
parent 1f38e1818b
commit 15e834060d
25 changed files with 4683 additions and 19 deletions

103
Cargo.lock generated
View File

@@ -141,12 +141,29 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "axum-inertia"
version = "0.8.1"
source = "git+https://github.com/LeNei/axum-inertia#6cfa00876604eff51701df87f0bea36d49fcdd8e"
dependencies = [
"axum",
"hex",
"http",
"hyper",
"indoc",
"maud",
"serde",
"serde_json",
"sha1",
]
[[package]] [[package]]
name = "axum-sqlx-template" name = "axum-sqlx-template"
version = "0.3.0" version = "0.3.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"axum", "axum",
"axum-inertia",
"chrono", "chrono",
"config", "config",
"http", "http",
@@ -160,7 +177,7 @@ dependencies = [
"tower-http", "tower-http",
"tracing", "tracing",
"tracing-bunyan-formatter", "tracing-bunyan-formatter",
"tracing-log 0.1.4", "tracing-log 0.2.0",
"tracing-subscriber", "tracing-subscriber",
] ]
@@ -696,6 +713,12 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "http-range-header"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
[[package]] [[package]]
name = "httparse" name = "httparse"
version = "1.10.1" version = "1.10.1"
@@ -916,6 +939,12 @@ dependencies = [
"hashbrown", "hashbrown",
] ]
[[package]]
name = "indoc"
version = "2.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd"
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.15" version = "1.0.15"
@@ -1006,6 +1035,28 @@ version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]]
name = "maud"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8156733e27020ea5c684db5beac5d1d611e1272ab17901a49466294b84fc217e"
dependencies = [
"itoa",
"maud_macros",
]
[[package]]
name = "maud_macros"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7261b00f3952f617899bc012e3dbd56e4f0110a038175929fa5d18e5a19913ca"
dependencies = [
"proc-macro2",
"proc-macro2-diagnostics",
"quote",
"syn",
]
[[package]] [[package]]
name = "md-5" name = "md-5"
version = "0.10.6" version = "0.10.6"
@@ -1028,6 +1079,16 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mime_guess"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
dependencies = [
"mime",
"unicase",
]
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.8.5" version = "0.8.5"
@@ -1254,6 +1315,18 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "proc-macro2-diagnostics"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
dependencies = [
"proc-macro2",
"quote",
"syn",
"version_check",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.40" version = "1.0.40"
@@ -2029,6 +2102,19 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "tokio-util"
version = "0.7.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034"
dependencies = [
"bytes",
"futures-core",
"futures-sink",
"pin-project-lite",
"tokio",
]
[[package]] [[package]]
name = "tower" name = "tower"
version = "0.5.2" version = "0.5.2"
@@ -2053,9 +2139,18 @@ checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"bytes", "bytes",
"futures-util",
"http", "http",
"http-body", "http-body",
"http-body-util",
"http-range-header",
"httpdate",
"mime",
"mime_guess",
"percent-encoding",
"pin-project-lite", "pin-project-lite",
"tokio",
"tokio-util",
"tower-layer", "tower-layer",
"tower-service", "tower-service",
"tracing", "tracing",
@@ -2170,6 +2265,12 @@ version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
[[package]]
name = "unicase"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
[[package]] [[package]]
name = "unicode-bidi" name = "unicode-bidi"
version = "0.3.18" version = "0.3.18"

View File

@@ -32,8 +32,9 @@ log = "0.4"
tracing = "0.1.19" tracing = "0.1.19"
tracing-subscriber = { version = "0.3", features = ["registry", "env-filter"] } tracing-subscriber = { version = "0.3", features = ["registry", "env-filter"] }
tracing-bunyan-formatter = "0.3" tracing-bunyan-formatter = "0.3"
tracing-log = "0.1.1" tracing-log = "0.2.0"
axum-inertia = { git = "https://github.com/LeNei/axum-inertia" }
axum = { version = "0.8.1", features = ["tracing"] } axum = { version = "0.8.1", features = ["tracing"] }
tower-http = { version = "0.6.2", features = ["trace", "cors"] } tower-http = { version = "0.6.2", features = ["trace", "cors", "fs"] }
http = "1.3.1" http = "1.3.1"
anyhow = "1.0" anyhow = "1.0"

View File

@@ -8,3 +8,4 @@ database:
password: "postgres" password: "postgres"
database_name: "postgres" database_name: "postgres"
require_ssl: false require_ssl: false
is_dev: false

View File

@@ -3,3 +3,4 @@ application:
base_url: "http://127.0.0.1" base_url: "http://127.0.0.1"
database: database:
require_ssl: false require_ssl: false
is_dev: true

24
frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

54
frontend/README.md Normal file
View File

@@ -0,0 +1,54 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default tseslint.config({
extends: [
// Remove ...tseslint.configs.recommended and replace with this
...tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
...tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
...tseslint.configs.stylisticTypeChecked,
],
languageOptions: {
// other options...
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
},
})
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default tseslint.config({
plugins: {
// Add the react-x and react-dom plugins
'react-x': reactX,
'react-dom': reactDom,
},
rules: {
// other rules...
// Enable its recommended typescript rules
...reactX.configs['recommended-typescript'].rules,
...reactDom.configs.recommended.rules,
},
})
```

28
frontend/eslint.config.js Normal file
View File

@@ -0,0 +1,28 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)

13
frontend/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

4201
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

34
frontend/package.json Normal file
View File

@@ -0,0 +1,34 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@inertiajs/react": "^2.0.5",
"@tailwindcss/vite": "^4.0.17",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tailwindcss": "^4.0.17",
"vite-plugin-compression2": "^1.3.3"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
"@types/node": "^22.13.14",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.21.0",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^15.15.0",
"typescript": "~5.7.2",
"typescript-eslint": "^8.24.1",
"vite": "^6.2.0"
}
}

1
frontend/public/vite.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,5 @@
"use client";
export default function Home() {
return <div className="text-red-500">Home</div>;
}

21
frontend/src/index.css Normal file
View File

@@ -0,0 +1,21 @@
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
html {
@apply h-full;
}
body,
#app {
@apply h-full bg-white dark:bg-zinc-900;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
input[type="number"] {
-moz-appearance: textfield;
}

13
frontend/src/main.tsx Normal file
View File

@@ -0,0 +1,13 @@
import { createInertiaApp } from "@inertiajs/react";
import { createRoot } from "react-dom/client";
import "./index.css";
createInertiaApp({
resolve: async (name) => {
const pages = import.meta.glob("./Pages/**/*.tsx");
return await pages[`./Pages/${name}.tsx`]();
},
setup({ el, App, props }) {
createRoot(el).render(<App {...props} />);
},
});

1
frontend/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

7
frontend/tsconfig.json Normal file
View File

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@@ -0,0 +1,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

29
frontend/vite.config.ts Normal file
View File

@@ -0,0 +1,29 @@
import { defineConfig } from "vite";
import path from "path";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
import viteCompression from "vite-plugin-compression2";
// https://vite.dev/config/
export default defineConfig({
plugins: [
react(),
tailwindcss(),
viteCompression({
include: /\.(js|css|html)$/,
}),
],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
build: {
// generiert .vite/manifest.json in outDir
manifest: true,
rollupOptions: {
// Überschreibe den Standard-.html-Einstieg
input: path.resolve(__dirname, "./src/main.tsx"),
},
},
});

View File

@@ -1,28 +1,22 @@
use axum::{Router, routing::get}; use axum::{Router, routing::get};
use http::{Method, StatusCode}; use http::{Method, StatusCode};
use sqlx::PgPool;
use tower_http::cors::{Any, CorsLayer}; use tower_http::cors::{Any, CorsLayer};
use tower_http::trace::TraceLayer;
use crate::config::ApiContext;
#[tracing::instrument(name = "Ping")] #[tracing::instrument(name = "Ping")]
async fn ping() -> StatusCode { async fn ping() -> StatusCode {
StatusCode::OK StatusCode::OK
} }
#[derive(Clone)] pub fn routes(api_context: ApiContext) -> Router {
pub struct ApiContext {
pub db: PgPool,
}
pub fn build_routes(api_context: ApiContext) -> Router {
let cors = CorsLayer::new() let cors = CorsLayer::new()
// allow `GET` and `POST` when accessing the resource // allow `GET` and `POST` when accessing the resource
.allow_methods([Method::GET, Method::POST]) .allow_methods([Method::GET, Method::POST])
// allow requests from any origin // allow requests from any origin
.allow_origin(Any); .allow_origin(Any);
Router::new() Router::new()
.route("/", get(ping)) .route("/ping", get(ping))
.layer(TraceLayer::new_for_http())
.layer(cors) .layer(cors)
.with_state(api_context) .with_state(api_context)
} }

17
src/config/inertia.rs Normal file
View File

@@ -0,0 +1,17 @@
use axum_inertia::{InertiaConfig, vite};
pub fn get_inertia_config(is_dev: bool) -> InertiaConfig {
if !is_dev {
vite::Production::new("frontend/dist/.vite/manifest.json", "src/main.tsx")
.unwrap()
.lang("de")
.into_config()
} else {
vite::Development::default()
.port(5173)
.main("src/main.tsx")
.lang("de")
.react() // call if using react
.into_config()
}
}

View File

@@ -1,15 +1,20 @@
pub mod app; pub mod app;
pub mod database; pub mod database;
pub mod inertia;
pub mod logging; pub mod logging;
use app::ApplicationSettings; use app::ApplicationSettings;
use axum::extract::FromRef;
use axum_inertia::InertiaConfig;
use database::DatabaseSettings; use database::DatabaseSettings;
use serde::Deserialize; use serde::Deserialize;
use sqlx::PgPool;
#[derive(Deserialize, Clone)] #[derive(Deserialize, Clone)]
pub struct Settings { pub struct Settings {
pub database: DatabaseSettings, pub database: DatabaseSettings,
pub application: ApplicationSettings, pub application: ApplicationSettings,
pub is_dev: bool,
} }
pub fn get_configuration() -> Result<Settings, config::ConfigError> { pub fn get_configuration() -> Result<Settings, config::ConfigError> {
@@ -74,3 +79,15 @@ impl TryFrom<String> for Environment {
} }
} }
} }
#[derive(Clone)]
pub struct ApiContext {
pub db: PgPool,
pub inertia: InertiaConfig,
}
impl FromRef<ApiContext> for InertiaConfig {
fn from_ref(app_state: &ApiContext) -> InertiaConfig {
app_state.inertia.clone()
}
}

View File

@@ -1,3 +1,4 @@
pub mod api;
pub mod config; pub mod config;
pub mod routes; pub mod pages;
pub mod startup; pub mod startup;

22
src/pages/mod.rs Normal file
View File

@@ -0,0 +1,22 @@
use axum::{Router, response::IntoResponse, routing::get};
use axum_inertia::Inertia;
use http::Method;
use serde_json::json;
use tower_http::cors::CorsLayer;
use crate::config::ApiContext;
#[tracing::instrument(name = "Home Page", skip(i))]
async fn home(i: Inertia) -> impl IntoResponse {
i.render("Home", json!({}))
}
pub fn routes(api_context: ApiContext) -> Router {
let cors = CorsLayer::new()
// allow `GET` and `POST` when accessing the resource
.allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE]);
Router::new()
.route("/", get(home))
.layer(cors)
.with_state(api_context)
}

View File

@@ -1,21 +1,49 @@
use crate::config::Settings; use crate::api::routes as api_routes;
use crate::routes::{build_routes, ApiContext}; use crate::config::inertia::get_inertia_config;
use crate::config::{ApiContext, Settings};
use crate::pages::routes as page_routes;
use tower_http::services::ServeDir;
use anyhow::Context; use anyhow::Context;
use axum::Router; use axum::Router;
use tokio::net::TcpListener; use tokio::net::TcpListener;
use tower_http::trace::TraceLayer;
pub async fn build(settings: Settings) -> anyhow::Result<()> { pub async fn build(settings: Settings) -> anyhow::Result<()> {
let api_context = ApiContext { let api_context = ApiContext {
db: settings.database.get_connection_pool(), db: settings.database.get_connection_pool(),
inertia: get_inertia_config(settings.is_dev),
}; };
let api_router = build_routes(api_context);
tracing::info!("Creating router...");
let mut router = Router::new()
.merge(page_routes(api_context.clone()))
.nest("/api", api_routes(api_context))
.layer(TraceLayer::new_for_http());
if !settings.is_dev {
// Serve static assets in production from the frontend/dist/assets directory instead of Vite
let service = ServeDir::new("frontend/dist/assets").precompressed_gzip();
router = router.nest_service("/assets", service);
}
let public_service = match settings.is_dev {
true => ServeDir::new("frontend/public"),
false => ServeDir::new("frontend/dist"),
};
router = router.fallback_service(public_service);
let address = format!( let address = format!(
"{}:{}", "{}:{}",
settings.application.host, settings.application.port settings.application.host, settings.application.port
); );
let listener = TcpListener::bind(address).await.context("Failed to bind to port")?;
run(api_router, listener).await tracing::info!("Binding to address: {}", address);
let listener = TcpListener::bind(address)
.await
.context("Failed to bind to port")?;
run(router, listener).await
} }
async fn run(router: Router, listener: TcpListener) -> anyhow::Result<()> { async fn run(router: Router, listener: TcpListener) -> anyhow::Result<()> {