This commit is contained in:
despiegk 2025-04-19 19:11:13 +02:00
parent 8f8016e748
commit 86bb165bf2
20 changed files with 3803 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
target/

2742
actix_mvc_app/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

18
actix_mvc_app/Cargo.toml Normal file
View File

@ -0,0 +1,18 @@
[package]
name = "actix_mvc_app"
version = "0.1.0"
edition = "2024"
[dependencies]
actix-web = "4.5.1"
actix-files = "0.6.5"
tera = "1.19.1"
serde = { version = "1.0.197", features = ["derive"] }
serde_json = "1.0.114"
env_logger = "0.11.2"
log = "0.4.21"
dotenv = "0.15.0"
chrono = { version = "0.4.35", features = ["serde"] }
config = "0.14.0"
num_cpus = "1.16.0"
futures = "0.3.30"

85
actix_mvc_app/README.md Normal file
View File

@ -0,0 +1,85 @@
# Actix MVC App
A Rust web application built with Actix Web, Tera templates, and Bootstrap 5.3.5, following the MVC (Model-View-Controller) architectural pattern.
## Features
- **Actix Web**: A powerful, pragmatic, and extremely fast web framework for Rust
- **Tera Templates**: A template engine inspired by Jinja2 and Django templates
- **Bootstrap 5.3.5**: A popular CSS framework for responsive web design
- **MVC Architecture**: Clean separation of concerns with Models, Views, and Controllers
- **Middleware Support**: Custom middleware for request timing and security headers
- **Configuration Management**: Flexible configuration system with environment variable support
- **Static File Serving**: Serve CSS, JavaScript, and other static assets
## Project Structure
```
actix_mvc_app/
├── Cargo.toml # Project dependencies
├── src/
│ ├── config/ # Configuration management
│ ├── controllers/ # Request handlers
│ ├── middleware/ # Custom middleware components
│ ├── models/ # Data models and business logic
│ ├── routes/ # Route definitions
│ ├── static/ # Static assets (CSS, JS, images)
│ │ ├── css/ # CSS files including Bootstrap
│ │ ├── js/ # JavaScript files
│ │ └── images/ # Image files
│ ├── utils/ # Utility functions
│ ├── views/ # Tera templates
│ └── main.rs # Application entry point
```
## Getting Started
### Prerequisites
- Rust and Cargo (latest stable version)
### Installation
1. Clone the repository:
```
git clone https://github.com/yourusername/actix_mvc_app.git
cd actix_mvc_app
```
2. Build the project:
```
cargo build
```
3. Run the application:
```
cargo run
```
4. Open your browser and navigate to `http://localhost:8080`
## Configuration
The application can be configured using environment variables or configuration files. The following environment variables are supported:
- `APP__SERVER__HOST`: The host address to bind to (default: 127.0.0.1)
- `APP__SERVER__PORT`: The port to listen on (default: 8080)
- `APP__SERVER__WORKERS`: The number of worker threads (default: number of CPU cores)
- `APP__TEMPLATES__DIR`: The directory containing templates (default: ./src/views)
## Development
### Adding a New Page
1. Create a new template in the `src/views` directory
2. Add a new handler method in the appropriate controller
3. Add a new route in the `src/routes/mod.rs` file
### Adding a New Model
1. Create a new model file in the `src/models` directory
2. Add the model to the `src/models/mod.rs` file
## License
This project is licensed under the MIT License - see the LICENSE file for details.

View File

@ -0,0 +1,129 @@
use actix_web::{
dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
Error,
};
use futures::future::{ready, LocalBoxFuture, Ready};
use std::{
future::Future,
pin::Pin,
time::Instant,
};
/// Middleware for logging request duration
pub struct RequestTimer;
impl<S, B> Transform<S, ServiceRequest> for RequestTimer
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Transform = RequestTimerMiddleware<S>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(RequestTimerMiddleware { service }))
}
}
pub struct RequestTimerMiddleware<S> {
service: S,
}
impl<S, B> Service<ServiceRequest> for RequestTimerMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
forward_ready!(service);
fn call(&self, req: ServiceRequest) -> Self::Future {
let start = Instant::now();
let path = req.path().to_owned();
let method = req.method().to_string();
let fut = self.service.call(req);
Box::pin(async move {
let res = fut.await?;
let duration = start.elapsed();
log::info!(
"{} {} - {} - {:?}",
method,
path,
res.status().as_u16(),
duration
);
Ok(res)
})
}
}
/// Middleware for adding security headers
pub struct SecurityHeaders;
impl<S, B> Transform<S, ServiceRequest> for SecurityHeaders
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Transform = SecurityHeadersMiddleware<S>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(SecurityHeadersMiddleware { service }))
}
}
pub struct SecurityHeadersMiddleware<S> {
service: S,
}
impl<S, B> Service<ServiceRequest> for SecurityHeadersMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Future = Pin<Box<dyn Future<Output = Result<ServiceResponse<B>, Error>>>>;
forward_ready!(service);
fn call(&self, req: ServiceRequest) -> Self::Future {
let fut = self.service.call(req);
Box::pin(async move {
let mut res = fut.await?;
// Add security headers
res.headers_mut().insert(
actix_web::http::header::X_CONTENT_TYPE_OPTIONS,
actix_web::http::header::HeaderValue::from_static("nosniff"),
);
res.headers_mut().insert(
actix_web::http::header::X_FRAME_OPTIONS,
actix_web::http::header::HeaderValue::from_static("DENY"),
);
res.headers_mut().insert(
actix_web::http::header::HeaderName::from_static("x-xss-protection"),
actix_web::http::header::HeaderValue::from_static("1; mode=block"),
);
Ok(res)
})
}
}

View File

@ -0,0 +1,64 @@
use std::env;
use config::{Config, ConfigError, File};
use serde::Deserialize;
/// Application configuration
#[derive(Debug, Deserialize, Clone)]
pub struct AppConfig {
/// Server configuration
pub server: ServerConfig,
/// Template configuration
pub templates: TemplateConfig,
}
/// Server configuration
#[derive(Debug, Deserialize, Clone)]
pub struct ServerConfig {
/// Host address to bind to
pub host: String,
/// Port to listen on
pub port: u16,
/// Workers count
pub workers: Option<u32>,
}
/// Template configuration
#[derive(Debug, Deserialize, Clone)]
pub struct TemplateConfig {
/// Directory containing templates
pub dir: String,
}
impl AppConfig {
/// Loads configuration from files and environment variables
pub fn new() -> Result<Self, ConfigError> {
// Set default values
let mut config_builder = Config::builder()
.set_default("server.host", "127.0.0.1")?
.set_default("server.port", 8080)?
.set_default("server.workers", None::<u32>)?
.set_default("templates.dir", "./src/views")?;
// Load from config file if it exists
if let Ok(config_path) = env::var("APP_CONFIG") {
config_builder = config_builder.add_source(File::with_name(&config_path));
} else {
// Try to load from default locations
config_builder = config_builder
.add_source(File::with_name("config/default").required(false))
.add_source(File::with_name("config/local").required(false));
}
// Override with environment variables (e.g., SERVER__HOST, SERVER__PORT)
config_builder = config_builder.add_source(config::Environment::with_prefix("APP").separator("__"));
// Build and deserialize the config
let config = config_builder.build()?;
config.try_deserialize()
}
}
/// Returns the application configuration
pub fn get_config() -> AppConfig {
AppConfig::new().expect("Failed to load configuration")
}

View File

@ -0,0 +1,88 @@
use actix_web::{web, HttpResponse, Responder, Result};
use tera::Tera;
use crate::models::User;
/// Controller for handling home-related routes
pub struct HomeController;
impl HomeController {
/// Handles the home page route
pub async fn index(tmpl: web::Data<Tera>) -> Result<impl Responder> {
let mut ctx = tera::Context::new();
ctx.insert("active_page", "home");
// Example of using models in controllers
let example_user = User::new("John Doe".to_string(), "john@example.com".to_string());
ctx.insert("user", &example_user);
let rendered = tmpl.render("index.html", &ctx)
.map_err(|e| {
eprintln!("Template rendering error: {}", e);
actix_web::error::ErrorInternalServerError("Template rendering error")
})?;
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
}
/// Handles the about page route
pub async fn about(tmpl: web::Data<Tera>) -> Result<impl Responder> {
let mut ctx = tera::Context::new();
ctx.insert("active_page", "about");
let rendered = tmpl.render("about.html", &ctx)
.map_err(|e| {
eprintln!("Template rendering error: {}", e);
actix_web::error::ErrorInternalServerError("Template rendering error")
})?;
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
}
/// Handles the contact page route
pub async fn contact(tmpl: web::Data<Tera>) -> Result<impl Responder> {
let mut ctx = tera::Context::new();
ctx.insert("active_page", "contact");
let rendered = tmpl.render("contact.html", &ctx)
.map_err(|e| {
eprintln!("Template rendering error: {}", e);
actix_web::error::ErrorInternalServerError("Template rendering error")
})?;
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
}
/// Handles form submissions from the contact page
pub async fn submit_contact(
form: web::Form<ContactForm>,
tmpl: web::Data<Tera>
) -> Result<impl Responder> {
// In a real application, you would process the form data here
// For example, save it to a database or send an email
println!("Contact form submission: {:?}", form);
// For this example, we'll just redirect back to the contact page
// with a success message
let mut ctx = tera::Context::new();
ctx.insert("active_page", "contact");
ctx.insert("success_message", "Your message has been sent successfully!");
let rendered = tmpl.render("contact.html", &ctx)
.map_err(|e| {
eprintln!("Template rendering error: {}", e);
actix_web::error::ErrorInternalServerError("Template rendering error")
})?;
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
}
}
/// Represents the data submitted in the contact form
#[derive(Debug, serde::Deserialize)]
pub struct ContactForm {
pub name: String,
pub email: String,
pub subject: String,
pub message: String,
}

View File

@ -0,0 +1,5 @@
// Export controllers
pub mod home;
// Re-export controllers for easier imports
pub use home::HomeController;

60
actix_mvc_app/src/main.rs Normal file
View File

@ -0,0 +1,60 @@
use actix_files as fs;
use actix_web::{App, HttpServer, web};
use actix_web::middleware::Logger;
use tera::Tera;
use std::io;
mod config;
mod controllers;
mod app_middleware;
mod models;
mod routes;
mod utils;
// Import middleware components
use app_middleware::{RequestTimer, SecurityHeaders};
#[actix_web::main]
async fn main() -> io::Result<()> {
// Initialize environment
dotenv::dotenv().ok();
env_logger::init_from_env(env_logger::Env::default().default_filter_or("info"));
// Load configuration
let config = config::get_config();
let bind_address = format!("{}:{}", config.server.host, config.server.port);
log::info!("Starting server at http://{}", bind_address);
// Create and configure the HTTP server
HttpServer::new(move || {
// Initialize Tera templates
let mut tera = match Tera::new(&format!("{}/**/*.html", config.templates.dir)) {
Ok(t) => t,
Err(e) => {
log::error!("Parsing error(s): {}", e);
::std::process::exit(1);
}
};
// Register custom Tera functions
utils::register_tera_functions(&mut tera);
App::new()
// Enable logger middleware
.wrap(Logger::default())
// Add custom middleware
.wrap(RequestTimer)
.wrap(SecurityHeaders)
// Configure static files
.service(fs::Files::new("/static", "./src/static"))
// Add Tera template engine
.app_data(web::Data::new(tera))
// Configure routes
.configure(routes::configure_routes)
})
.bind(bind_address)?
.workers(num_cpus::get())
.run()
.await
}

View File

@ -0,0 +1,130 @@
use actix_web::{
dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
Error,
};
use futures::future::{ready, LocalBoxFuture, Ready};
use std::{
future::Future,
pin::Pin,
task::{Context, Poll},
time::Instant,
};
/// Middleware for logging request duration
pub struct RequestTimer;
impl<S, B> Transform<S, ServiceRequest> for RequestTimer
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Transform = RequestTimerMiddleware<S>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(RequestTimerMiddleware { service }))
}
}
pub struct RequestTimerMiddleware<S> {
service: S,
}
impl<S, B> Service<ServiceRequest> for RequestTimerMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
forward_ready!(service);
fn call(&self, req: ServiceRequest) -> Self::Future {
let start = Instant::now();
let path = req.path().to_owned();
let method = req.method().to_string();
let fut = self.service.call(req);
Box::pin(async move {
let res = fut.await?;
let duration = start.elapsed();
log::info!(
"{} {} - {} - {:?}",
method,
path,
res.status().as_u16(),
duration
);
Ok(res)
})
}
}
/// Middleware for adding security headers
pub struct SecurityHeaders;
impl<S, B> Transform<S, ServiceRequest> for SecurityHeaders
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Transform = SecurityHeadersMiddleware<S>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(SecurityHeadersMiddleware { service }))
}
}
pub struct SecurityHeadersMiddleware<S> {
service: S,
}
impl<S, B> Service<ServiceRequest> for SecurityHeadersMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Future = Pin<Box<dyn Future<Output = Result<ServiceResponse<B>, Error>>>>;
forward_ready!(service);
fn call(&self, req: ServiceRequest) -> Self::Future {
let fut = self.service.call(req);
Box::pin(async move {
let mut res = fut.await?;
// Add security headers
res.headers_mut().insert(
actix_web::http::header::X_CONTENT_TYPE_OPTIONS,
actix_web::http::header::HeaderValue::from_static("nosniff"),
);
res.headers_mut().insert(
actix_web::http::header::X_FRAME_OPTIONS,
actix_web::http::header::HeaderValue::from_static("DENY"),
);
res.headers_mut().insert(
actix_web::http::header::HeaderName::from_static("x-xss-protection"),
actix_web::http::header::HeaderValue::from_static("1; mode=block"),
);
Ok(res)
})
}
}

View File

@ -0,0 +1,5 @@
// Export models
pub mod user;
// Re-export models for easier imports
pub use user::User;

View File

@ -0,0 +1,105 @@
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};
/// Represents a user in the system
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
/// Unique identifier for the user
pub id: Option<i32>,
/// User's full name
pub name: String,
/// User's email address
pub email: String,
/// User's role in the system
pub role: UserRole,
/// When the user was created
pub created_at: Option<DateTime<Utc>>,
/// When the user was last updated
pub updated_at: Option<DateTime<Utc>>,
}
/// Represents the possible roles a user can have
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum UserRole {
/// Regular user with limited permissions
User,
/// Administrator with full permissions
Admin,
}
impl User {
/// Creates a new user with default values
pub fn new(name: String, email: String) -> Self {
Self {
id: None,
name,
email,
role: UserRole::User,
created_at: Some(Utc::now()),
updated_at: Some(Utc::now()),
}
}
/// Creates a new admin user
pub fn new_admin(name: String, email: String) -> Self {
Self {
id: None,
name,
email,
role: UserRole::Admin,
created_at: Some(Utc::now()),
updated_at: Some(Utc::now()),
}
}
/// Checks if the user is an admin
pub fn is_admin(&self) -> bool {
self.role == UserRole::Admin
}
/// Updates the user's information
pub fn update(&mut self, name: Option<String>, email: Option<String>) {
if let Some(name) = name {
self.name = name;
}
if let Some(email) = email {
self.email = email;
}
self.updated_at = Some(Utc::now());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_user() {
let user = User::new("John Doe".to_string(), "john@example.com".to_string());
assert_eq!(user.name, "John Doe");
assert_eq!(user.email, "john@example.com");
assert!(!user.is_admin());
}
#[test]
fn test_new_admin() {
let admin = User::new_admin("Admin User".to_string(), "admin@example.com".to_string());
assert_eq!(admin.name, "Admin User");
assert_eq!(admin.email, "admin@example.com");
assert!(admin.is_admin());
}
#[test]
fn test_update_user() {
let mut user = User::new("John Doe".to_string(), "john@example.com".to_string());
user.update(Some("Jane Doe".to_string()), None);
assert_eq!(user.name, "Jane Doe");
assert_eq!(user.email, "john@example.com");
user.update(None, Some("jane@example.com".to_string()));
assert_eq!(user.name, "Jane Doe");
assert_eq!(user.email, "jane@example.com");
}
}

View File

@ -0,0 +1,16 @@
use actix_web::web;
use crate::controllers::home::HomeController;
/// Configures all application routes
pub fn configure_routes(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("")
// Home routes
.route("/", web::get().to(HomeController::index))
.route("/about", web::get().to(HomeController::about))
.route("/contact", web::get().to(HomeController::contact))
.route("/contact", web::post().to(HomeController::submit_contact))
// Add more routes here as needed
);
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,103 @@
use chrono::{DateTime, TimeZone, Utc};
use tera::{self, Function, Result, Value};
/// Registers custom Tera functions
pub fn register_tera_functions(tera: &mut tera::Tera) {
tera.register_function("now", NowFunction);
tera.register_function("format_date", FormatDateFunction);
}
/// Tera function to get the current date/time
#[derive(Clone)]
pub struct NowFunction;
impl Function for NowFunction {
fn call(&self, args: &std::collections::HashMap<String, Value>) -> Result<Value> {
let format = match args.get("format") {
Some(val) => match val.as_str() {
Some(s) => s,
None => "%Y-%m-%d %H:%M:%S",
},
None => "%Y-%m-%d %H:%M:%S",
};
let now = Utc::now();
// Special case for just getting the year
if args.get("year").and_then(|v| v.as_bool()).unwrap_or(false) {
return Ok(Value::String(now.format("%Y").to_string()));
}
Ok(Value::String(now.format(format).to_string()))
}
}
/// Tera function to format a date
#[derive(Clone)]
pub struct FormatDateFunction;
impl Function for FormatDateFunction {
fn call(&self, args: &std::collections::HashMap<String, Value>) -> Result<Value> {
let timestamp = match args.get("timestamp") {
Some(val) => match val.as_i64() {
Some(ts) => ts,
None => {
return Err(tera::Error::msg(
"The 'timestamp' argument must be a valid timestamp",
))
}
},
None => {
return Err(tera::Error::msg(
"The 'timestamp' argument is required",
))
}
};
let format = match args.get("format") {
Some(val) => match val.as_str() {
Some(s) => s,
None => "%Y-%m-%d %H:%M:%S",
},
None => "%Y-%m-%d %H:%M:%S",
};
// Convert timestamp to DateTime using the non-deprecated method
let datetime = match DateTime::from_timestamp(timestamp, 0) {
Some(dt) => dt,
None => {
return Err(tera::Error::msg(
"Failed to convert timestamp to datetime",
))
}
};
Ok(Value::String(datetime.format(format).to_string()))
}
}
/// Formats a date for display
pub fn format_date(date: &DateTime<Utc>, format: &str) -> String {
date.format(format).to_string()
}
/// Truncates a string to a maximum length and adds an ellipsis if truncated
pub fn truncate_string(s: &str, max_length: usize) -> String {
if s.len() <= max_length {
s.to_string()
} else {
format!("{}...", &s[0..max_length])
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_truncate_string() {
assert_eq!(truncate_string("Hello", 10), "Hello");
assert_eq!(truncate_string("Hello, world!", 5), "Hello...");
assert_eq!(truncate_string("", 5), "");
}
}

View File

@ -0,0 +1,62 @@
{% extends "base.html" %}
{% block title %}About - Actix MVC App{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-body">
<h1 class="card-title">About Actix MVC App</h1>
<p class="card-text">This is a sample application demonstrating how to build a web application using Rust with an MVC architecture.</p>
<h2 class="mt-4">Technology Stack</h2>
<div class="row">
<div class="col-md-6">
<div class="card mb-3">
<div class="card-header">Backend</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">
<strong>Actix Web</strong> - A powerful, pragmatic, and extremely fast web framework for Rust
</li>
<li class="list-group-item">
<strong>Tera</strong> - A template engine inspired by Jinja2 and Django templates
</li>
<li class="list-group-item">
<strong>Serde</strong> - A framework for serializing and deserializing Rust data structures
</li>
</ul>
</div>
</div>
<div class="col-md-6">
<div class="card mb-3">
<div class="card-header">Frontend</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">
<strong>Bootstrap 5.3.5</strong> - A popular CSS framework for responsive web design
</li>
<li class="list-group-item">
<strong>HTML5</strong> - The latest version of the HTML standard
</li>
<li class="list-group-item">
<strong>CSS3</strong> - The latest evolution of the Cascading Style Sheets language
</li>
</ul>
</div>
</div>
</div>
<h2 class="mt-4">MVC Architecture</h2>
<p>This application follows the Model-View-Controller (MVC) architectural pattern:</p>
<ul>
<li><strong>Model:</strong> Represents the data and business logic of the application</li>
<li><strong>View:</strong> Represents the UI of the application (Tera templates)</li>
<li><strong>Controller:</strong> Handles the user request, works with the Model, and selects a View to render</li>
</ul>
<a href="/contact" class="btn btn-primary mt-3">Contact Us</a>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,54 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Actix MVC App{% endblock %}</title>
<link rel="stylesheet" href="/static/css/bootstrap.min.css">
{% block extra_css %}{% endblock %}
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="/">Actix MVC App</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link {% if active_page == 'home' %}active{% endif %}" href="/">Home</a>
</li>
<li class="nav-item">
<a class="nav-link {% if active_page == 'about' %}active{% endif %}" href="/about">About</a>
</li>
<li class="nav-item">
<a class="nav-link {% if active_page == 'contact' %}active{% endif %}" href="/contact">Contact</a>
</li>
</ul>
</div>
</div>
</nav>
<main class="container py-4">
{% block content %}{% endblock %}
</main>
<footer class="bg-dark text-white py-4 mt-5">
<div class="container">
<div class="row">
<div class="col-md-6">
<h5>Actix MVC App</h5>
<p>A Rust web application using Actix Web, Tera templates, and Bootstrap.</p>
</div>
<div class="col-md-6 text-md-end">
<p>&copy; {{ now(year=true) }} Actix MVC App. All rights reserved.</p>
</div>
</div>
</div>
</footer>
<script src="/static/js/bootstrap.bundle.min.js"></script>
{% block extra_js %}{% endblock %}
</body>
</html>

View File

@ -0,0 +1,68 @@
{% extends "base.html" %}
{% block title %}Contact - Actix MVC App{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-8 offset-md-2">
<div class="card">
<div class="card-body">
<h1 class="card-title">Contact Us</h1>
<p class="card-text">Have questions or feedback? Feel free to reach out to us using the form below.</p>
<form id="contactForm" class="mt-4">
<div class="mb-3">
<label for="name" class="form-label">Name</label>
<input type="text" class="form-control" id="name" name="name" required>
</div>
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<div class="mb-3">
<label for="subject" class="form-label">Subject</label>
<input type="text" class="form-control" id="subject" name="subject" required>
</div>
<div class="mb-3">
<label for="message" class="form-label">Message</label>
<textarea class="form-control" id="message" name="message" rows="5" required></textarea>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
<div class="mt-5">
<h3>Other Ways to Connect</h3>
<div class="row mt-3">
<div class="col-md-6">
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title">Email</h5>
<p class="card-text">info@example.com</p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title">GitHub</h5>
<p class="card-text">github.com/example/actix-mvc-app</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% block extra_js %}
<script>
document.getElementById('contactForm').addEventListener('submit', function(e) {
e.preventDefault();
alert('This is a demo form. In a real application, this would submit data to the server.');
});
</script>
{% endblock %}
{% endblock %}

View File

@ -0,0 +1,55 @@
{% extends "base.html" %}
{% block title %}Home - Actix MVC App{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-body">
<h1 class="card-title">Welcome to Actix MVC App</h1>
<p class="card-text">This is a Rust web application built with:</p>
<ul class="list-group list-group-flush mb-4">
<li class="list-group-item">Actix Web - A powerful, pragmatic, and extremely fast web framework for Rust</li>
<li class="list-group-item">Tera Templates - A template engine inspired by Jinja2 and Django templates</li>
<li class="list-group-item">Bootstrap 5.3.5 - A popular CSS framework for responsive web design</li>
</ul>
<p>This application follows the MVC (Model-View-Controller) architectural pattern:</p>
<div class="row">
<div class="col-md-4">
<div class="card mb-3">
<div class="card-header bg-primary text-white">
Models
</div>
<div class="card-body">
<p>Data structures and business logic</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card mb-3">
<div class="card-header bg-success text-white">
Views
</div>
<div class="card-body">
<p>Tera templates for rendering HTML</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card mb-3">
<div class="card-header bg-info text-white">
Controllers
</div>
<div class="card-body">
<p>Request handlers and application logic</p>
</div>
</div>
</div>
</div>
<a href="/about" class="btn btn-primary">Learn More</a>
</div>
</div>
</div>
</div>
{% endblock %}