Working session
This commit is contained in:
parent
0d0845cc7b
commit
2849d9f339
159
Cargo.lock
generated
159
Cargo.lock
generated
@ -106,6 +106,22 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-identity"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b8ddc6f6a8b19c4016aaa13519968da9969bc3bc1c1c883cdb0f25dd6c8cf7"
|
||||
dependencies = [
|
||||
"actix-service",
|
||||
"actix-session",
|
||||
"actix-utils",
|
||||
"actix-web",
|
||||
"derive_more 1.0.0",
|
||||
"futures-core",
|
||||
"serde",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-macros"
|
||||
version = "0.2.4"
|
||||
@ -171,6 +187,24 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-session"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "efe6976a74f34f1b6d07a6c05aadc0ed0359304a7781c367fa5b4029418db08f"
|
||||
dependencies = [
|
||||
"actix-service",
|
||||
"actix-utils",
|
||||
"actix-web",
|
||||
"anyhow",
|
||||
"derive_more 1.0.0",
|
||||
"rand",
|
||||
"redis 0.26.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-tls"
|
||||
version = "3.4.0"
|
||||
@ -184,7 +218,7 @@ dependencies = [
|
||||
"impl-more",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-rustls 0.23.4",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"webpki-roots 0.22.6",
|
||||
@ -1072,6 +1106,8 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"actix",
|
||||
"actix-files",
|
||||
"actix-identity",
|
||||
"actix-session",
|
||||
"actix-web",
|
||||
"askama",
|
||||
"askama_actix",
|
||||
@ -1080,7 +1116,7 @@ dependencies = [
|
||||
"humantime",
|
||||
"humantime-serde",
|
||||
"migration",
|
||||
"redis",
|
||||
"redis 0.27.5",
|
||||
"rswind",
|
||||
"rswind_cli",
|
||||
"sea-orm",
|
||||
@ -1109,6 +1145,16 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
@ -1357,6 +1403,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.82",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2537,6 +2584,12 @@ version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||
|
||||
[[package]]
|
||||
name = "ordered-float"
|
||||
version = "3.9.2"
|
||||
@ -3052,6 +3105,34 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redis"
|
||||
version = "0.26.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e902a69d09078829137b4a5d9d082e0490393537badd7c91a3d69d14639e115f"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"async-trait",
|
||||
"bytes",
|
||||
"combine",
|
||||
"futures",
|
||||
"futures-util",
|
||||
"itoa",
|
||||
"num-bigint",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustls 0.23.15",
|
||||
"rustls-native-certs",
|
||||
"rustls-pemfile",
|
||||
"rustls-pki-types",
|
||||
"ryu",
|
||||
"tokio",
|
||||
"tokio-retry",
|
||||
"tokio-rustls 0.26.0",
|
||||
"tokio-util",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redis"
|
||||
version = "0.27.5"
|
||||
@ -3431,6 +3512,19 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-native-certs"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5"
|
||||
dependencies = [
|
||||
"openssl-probe",
|
||||
"rustls-pemfile",
|
||||
"rustls-pki-types",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "2.2.0"
|
||||
@ -3472,6 +3566,15 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "1.0.0-alpha.15"
|
||||
@ -3683,6 +3786,29 @@ version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework-sys"
|
||||
version = "2.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.23"
|
||||
@ -4321,6 +4447,17 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-retry"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f57eb36ecbe0fc510036adff84824dd3c24bb781e21bfa67b69d556aa85214f"
|
||||
dependencies = [
|
||||
"pin-project",
|
||||
"rand",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-retry2"
|
||||
version = "0.5.6"
|
||||
@ -4343,6 +4480,17 @@ dependencies = [
|
||||
"webpki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
|
||||
dependencies = [
|
||||
"rustls 0.23.15",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-stream"
|
||||
version = "0.1.16"
|
||||
@ -4510,6 +4658,12 @@ version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "unicode_categories"
|
||||
version = "0.1.1"
|
||||
@ -4562,6 +4716,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -15,7 +15,7 @@ tracing = "0.1.40"
|
||||
tracing-subscriber = "0.3.18"
|
||||
serde = "1.0.210"
|
||||
serde_json = "1.0.132"
|
||||
uuid = { version = "1.11.0", features = ["v4", "v8"] }
|
||||
uuid = { version = "1.11.0", features = ["serde", "v4", "v8"] }
|
||||
migration = { path = "./migration" }
|
||||
rswind = "0.0.1-alpha.1"
|
||||
rswind_cli = "0.0.1-alpha.1"
|
||||
@ -23,5 +23,7 @@ derive_more = { version = "1.0.0", features = ["deref"] }
|
||||
chrono = "0.4.38"
|
||||
humantime = "2.1.0"
|
||||
humantime-serde = "1.1.1"
|
||||
actix-session = { version = "0.10.1", features = ["redis-session-rustls"] }
|
||||
actix-identity = "0.8.0"
|
||||
|
||||
[build-dependencies]
|
||||
|
@ -2191,8 +2191,8 @@ html{
|
||||
height: 12rem;
|
||||
}
|
||||
|
||||
.md\:w-72{
|
||||
width: 18rem;
|
||||
.md\:w-3\/4{
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
.md\:w-\[225px\]{
|
||||
@ -2207,10 +2207,6 @@ html{
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.md\:border-2{
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.md\:text-left{
|
||||
text-align: left;
|
||||
}
|
||||
|
1
run
1
run
@ -1 +0,0 @@
|
||||
clear;RUST_LOG=debug BIND=0.0.0.0:7979 ADMINS=admin:admin PSQL=postgres://postgres@localhost/cooked REDIS=redis://localhost cargo run
|
8
scripts/run
Executable file
8
scripts/run
Executable file
@ -0,0 +1,8 @@
|
||||
clear
|
||||
export RUST_LOG=debug
|
||||
export BIND=0.0.0.0:7979
|
||||
export ADMINS=admin@example.com:admin
|
||||
export PSQL=postgres://postgres@localhost/cooked
|
||||
export REDIS=redis://localhost
|
||||
export SECRET='NSvunLCGGNWOn0tZVwGZghTc9uXJhSOok0SR2goue4ENK9wfWzFfz81eJflY+xrX'
|
||||
cargo run
|
@ -1,2 +1,2 @@
|
||||
yarn
|
||||
cargo watch -w ./templates -w ./src -w ./tailwind.config.js -c --shell "./render_css && ./run"
|
||||
cargo watch -w ./templates -w ./src -w ./tailwind.config.js -c --shell "./scripts/render_css && ./scripts/run"
|
137
src/main.rs
137
src/main.rs
@ -1,13 +1,16 @@
|
||||
use actix_files::Files;
|
||||
use actix_identity::{Identity, IdentityMiddleware};
|
||||
use actix_session::{storage::RedisSessionStore, SessionMiddleware};
|
||||
use actix_web::cookie::Key;
|
||||
use actix_web::{
|
||||
cookie::{time::Duration, CookieBuilder},
|
||||
get, post,
|
||||
web::{Data, Form, Json, Path},
|
||||
App, HttpResponse, HttpServer, Responder,
|
||||
};
|
||||
use actix_web::{HttpMessage, HttpRequest};
|
||||
use askama::Template;
|
||||
use redis::AsyncCommands;
|
||||
use sea_orm::{prelude::*, Database};
|
||||
use sea_orm::{prelude::*, Database, QuerySelect};
|
||||
use serde::Deserialize;
|
||||
use std::str::FromStr;
|
||||
|
||||
mod entities;
|
||||
@ -20,6 +23,18 @@ mod filters {
|
||||
}
|
||||
}
|
||||
|
||||
const SESSION_KEY: &'static str = "session";
|
||||
|
||||
type User = String;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
enum Page {
|
||||
Index,
|
||||
Recipe,
|
||||
Search,
|
||||
SignIn,
|
||||
}
|
||||
|
||||
#[derive(Debug, Template, derive_more::Deref)]
|
||||
#[template(path = "recipe_card.html")]
|
||||
struct RecipeCard(entities::recipies::Model);
|
||||
@ -28,6 +43,20 @@ struct RecipeCard(entities::recipies::Model);
|
||||
#[template(path = "index.html")]
|
||||
struct IndexTemplate {
|
||||
recipies: Vec<RecipeCard>,
|
||||
count: u64,
|
||||
session: Option<User>,
|
||||
page: Page,
|
||||
}
|
||||
#[derive(Debug, Template)]
|
||||
#[template(path = "top_bar.html")]
|
||||
struct TopBar<'s> {
|
||||
session: &'s Option<User>,
|
||||
}
|
||||
impl<'s> TopBar<'s> {
|
||||
pub fn new(session: &'s Option<User>) -> Self {
|
||||
tracing::info!("top bar session: {session:?}");
|
||||
Self { session }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@ -74,23 +103,31 @@ struct SignIn {
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Template)]
|
||||
#[derive(Debug, Template)]
|
||||
#[template(path = "sign_in/form.html")]
|
||||
struct SignInForm {
|
||||
not_found: bool,
|
||||
email: String,
|
||||
session: Option<User>,
|
||||
page: Page,
|
||||
}
|
||||
|
||||
#[get("/sign-in")]
|
||||
async fn render_sign_in() -> SignInForm {
|
||||
SignInForm::default()
|
||||
SignInForm {
|
||||
not_found: false,
|
||||
email: "".into(),
|
||||
session: None,
|
||||
page: Page::SignIn,
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/sign-in")]
|
||||
async fn sign_in(
|
||||
req: HttpRequest,
|
||||
admins: Data<Admins>,
|
||||
payload: Form<SignIn>,
|
||||
redis: Data<redis::Client>,
|
||||
admin: Option<Identity>,
|
||||
) -> HttpResponse {
|
||||
let payload = payload.into_inner();
|
||||
let is_admin = admins
|
||||
@ -99,33 +136,27 @@ async fn sign_in(
|
||||
.iter()
|
||||
.any(|admin| admin.email == payload.email && admin.pass == payload.password);
|
||||
if is_admin {
|
||||
let mut con = redis
|
||||
.get_multiplexed_async_connection()
|
||||
.await
|
||||
.expect("Failed to get redis get_multiplexed_async_connection");
|
||||
tracing::info!("Valid credentials");
|
||||
// let res = session
|
||||
// .insert(SESSION_KEY, payload.email.clone())
|
||||
// .inspect_err(|e| tracing::error!("Failed to save session: {e}"));
|
||||
// tracing::debug!("Saving session res: {res:?}");
|
||||
let _s =
|
||||
Identity::login(&req.extensions(), payload.email).expect("Failed to store session");
|
||||
|
||||
let uid = uuid::Uuid::new_v4();
|
||||
|
||||
let _: () = con
|
||||
.set_ex(uid.to_string(), "1", 60 * 60 * 24) // session exists for 1 day
|
||||
.await
|
||||
.expect("Failed to store session");
|
||||
HttpResponse::SeeOther()
|
||||
.cookie(
|
||||
CookieBuilder::new("ses", uid.to_string())
|
||||
.secure(true)
|
||||
.max_age(Duration::hours(24))
|
||||
.finish(),
|
||||
)
|
||||
.append_header(("location", "/"))
|
||||
.finish()
|
||||
} else {
|
||||
tracing::warn!("Invalid credentials");
|
||||
HttpResponse::BadRequest()
|
||||
.append_header(("Content-Type", "text/html"))
|
||||
.body(
|
||||
SignInForm {
|
||||
email: payload.email,
|
||||
not_found: true,
|
||||
session: admin.and_then(|s| s.id().ok()),
|
||||
page: Page::SignIn,
|
||||
}
|
||||
.render()
|
||||
.unwrap_or_default(),
|
||||
@ -133,14 +164,35 @@ async fn sign_in(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Padding {
|
||||
page: Option<u16>,
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
async fn index_html(db: Data<DatabaseConnection>) -> IndexTemplate {
|
||||
async fn index_html(
|
||||
db: Data<DatabaseConnection>,
|
||||
q: actix_web::web::Query<Padding>,
|
||||
admin: Option<Identity>,
|
||||
) -> IndexTemplate {
|
||||
let count = (entities::prelude::Recipies::find()
|
||||
.count(&**db)
|
||||
.await
|
||||
.unwrap_or_default() as f64
|
||||
/ 20.0)
|
||||
.ceil() as u64;
|
||||
let recipies = entities::prelude::Recipies::find()
|
||||
.limit(20)
|
||||
.offset(q.page.unwrap_or_default() as u64)
|
||||
.all(&**db)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
IndexTemplate {
|
||||
recipies: recipies.into_iter().map(RecipeCard).collect(),
|
||||
count,
|
||||
session: admin.and_then(|s| s.id().ok()),
|
||||
page: Page::Index,
|
||||
}
|
||||
}
|
||||
|
||||
@ -151,10 +203,16 @@ struct RecipeDetailTemplate {
|
||||
tags: Vec<entities::recipe_tags::Model>,
|
||||
steps: Vec<entities::recipe_steps::Model>,
|
||||
ingeredients: Vec<entities::recipe_ingeredients::Model>,
|
||||
session: Option<User>,
|
||||
page: Page,
|
||||
}
|
||||
|
||||
#[get("/recipe/{id}")]
|
||||
async fn show(id: Path<i64>, db: Data<DatabaseConnection>) -> impl Responder {
|
||||
async fn show(
|
||||
id: Path<i64>,
|
||||
db: Data<DatabaseConnection>,
|
||||
admin: Option<Identity>,
|
||||
) -> impl Responder {
|
||||
let id = id.into_inner();
|
||||
let db = &**db;
|
||||
let Ok(Some(recipe)) = entities::prelude::Recipies::find()
|
||||
@ -188,6 +246,8 @@ async fn show(id: Path<i64>, db: Data<DatabaseConnection>) -> impl Responder {
|
||||
tags,
|
||||
recipe,
|
||||
ingeredients,
|
||||
session: admin.and_then(|s| s.id().ok()),
|
||||
page: Page::Recipe,
|
||||
}
|
||||
.render()
|
||||
.unwrap_or_default(),
|
||||
@ -209,19 +269,6 @@ async fn main() {
|
||||
tracing::info!("You can define admins by setting env variable ADMINS");
|
||||
tracing::info!(" Example: ADMINS=login1:pass1,login2,pass2");
|
||||
|
||||
// {
|
||||
// use glob::GlobMatcher;
|
||||
// use rswind::*;
|
||||
|
||||
// let mut p = create_app();
|
||||
// p.processor.options.watch = false;
|
||||
// p.processor.options.parallel = true;
|
||||
// p.glob =
|
||||
// GlobMatcher::new(vec!["**/*.html"], "./templates".into()).expect("Glob must be valid");
|
||||
// let contents = p.generate_contents();
|
||||
// std::fs::write("./templates/styles.css", &contents).expect("Failed to save styles.css");
|
||||
// }
|
||||
|
||||
std::fs::create_dir_all("./pages").ok();
|
||||
std::fs::create_dir_all("./tmp").ok();
|
||||
|
||||
@ -233,6 +280,8 @@ async fn main() {
|
||||
std::env::var("PSQL").expect("PSQL is required. Please provide postgresql connection url");
|
||||
let redis_url =
|
||||
std::env::var("REDIS").expect("REDIS is required. Pleasde provide redis connection url");
|
||||
let secret =
|
||||
std::env::var("SECRET").expect("SECRET is required. Please provider encryption key");
|
||||
|
||||
// Build structs
|
||||
let admins = Admins::from_str(&admins).expect("Parsing admins should be successful here");
|
||||
@ -244,7 +293,11 @@ async fn main() {
|
||||
|
||||
Migrator::up(&db, None).await.unwrap();
|
||||
}
|
||||
let redis = redis::Client::open(redis_url).expect("Failed to connect to redis");
|
||||
let redis = redis::Client::open(redis_url.as_str()).expect("Failed to connect to redis");
|
||||
let secret_key = Key::from(secret.as_bytes());
|
||||
drop(secret);
|
||||
tracing::info!("{:?}", secret_key.master());
|
||||
let redis_store = RedisSessionStore::new(redis_url.as_str()).await.unwrap();
|
||||
|
||||
// Transform to data
|
||||
let admins = Data::new(admins);
|
||||
@ -257,6 +310,14 @@ async fn main() {
|
||||
.wrap(actix_web::middleware::Logger::default())
|
||||
.wrap(actix_web::middleware::NormalizePath::trim())
|
||||
.wrap(actix_web::middleware::Compress::default())
|
||||
.wrap(IdentityMiddleware::default())
|
||||
.wrap(
|
||||
SessionMiddleware::builder(redis_store.clone(), secret_key.clone())
|
||||
.cookie_http_only(true)
|
||||
.cookie_secure(false)
|
||||
.cookie_name(SESSION_KEY.to_string())
|
||||
.build(),
|
||||
)
|
||||
.app_data(admins.clone())
|
||||
.app_data(db.clone())
|
||||
.app_data(redis.clone())
|
||||
|
@ -2,9 +2,7 @@
|
||||
|
||||
{% block content %}
|
||||
<main class="md:col-span-3 flex flex-col gap-4 m-4">
|
||||
<div class="flex justify-center md:justify-end">
|
||||
<a href="/sign-in" class="btn md:border-2 hover:shadow-lg transition ease-out duration-300">Login</a>
|
||||
</div>
|
||||
{{ TopBar::new(session)|safe }}
|
||||
|
||||
<header class="flex flex-col gap-2">
|
||||
<h2 class="text-gray-600 text-6xl font-semibold text-center">Recipes</h2>
|
||||
@ -16,6 +14,15 @@
|
||||
{{ recipe|safe }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="flex gap-4 flex-wrap">
|
||||
{% match count %}
|
||||
{% when 0 %}
|
||||
{% when _ %}
|
||||
{% for page in 0..count %}
|
||||
<a class="btn" href="/?page={{page}}">{{page}}</a>
|
||||
{% endfor %}
|
||||
{% endmatch %}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center">
|
||||
<div class="btn bg-secondary-100 text-secondary-200 hover:shadow-inner transform hover:scale-110 hover:bg-opacity-60 transition ease-out duration-300">Load More</div>
|
||||
|
@ -21,7 +21,16 @@
|
||||
d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" />
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/search" class="px-4 flex justify-end border-r-4 border-primary hover:shadow-md">
|
||||
<span>Search</span>
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="w-5 ml-2">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.25007 2.38782C8.54878 2.0992 10.1243 2 12 2C13.8757 2 15.4512 2.0992 16.7499 2.38782C18.06 2.67897 19.1488 3.176 19.9864 4.01358C20.824 4.85116 21.321 5.94002 21.6122 7.25007C21.9008 8.54878 22 10.1243 22 12C22 13.8757 21.9008 15.4512 21.6122 16.7499C21.321 18.06 20.824 19.1488 19.9864 19.9864C19.1488 20.824 18.06 21.321 16.7499 21.6122C15.4512 21.9008 13.8757 22 12 22C10.1243 22 8.54878 21.9008 7.25007 21.6122C5.94002 21.321 4.85116 20.824 4.01358 19.9864C3.176 19.1488 2.67897 18.06 2.38782 16.7499C2.0992 15.4512 2 13.8757 2 12C2 10.1243 2.0992 8.54878 2.38782 7.25007C2.67897 5.94002 3.176 4.85116 4.01358 4.01358C4.85116 3.176 5.94002 2.67897 7.25007 2.38782ZM9 11.5C9 10.1193 10.1193 9 11.5 9C12.8807 9 14 10.1193 14 11.5C14 12.8807 12.8807 14 11.5 14C10.1193 14 9 12.8807 9 11.5ZM11.5 7C9.01472 7 7 9.01472 7 11.5C7 13.9853 9.01472 16 11.5 16C12.3805 16 13.202 15.7471 13.8957 15.31L15.2929 16.7071C15.6834 17.0976 16.3166 17.0976 16.7071 16.7071C17.0976 16.3166 17.0976 15.6834 16.7071 15.2929L15.31 13.8957C15.7471 13.202 16 12.3805 16 11.5C16 9.01472 13.9853 7 11.5 7Z" fill="#323232"/>
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
@ -5,7 +5,7 @@
|
||||
{% include "../top_bar.html" %}
|
||||
<div class="sm:rounded-3xl w-full flex flex-col gap-4">
|
||||
<div class="text-center w-full">
|
||||
<img src={{ recipe.image_url.clone() }} alt="{{recipe.title}}" class="rounded-xl w-40 md:w-72 ml-auto mr-auto">
|
||||
<img src={{ recipe.image_url.clone() }} alt="{{recipe.title}}" class="rounded-xl w-40 md:w-3/4 ml-auto mr-auto">
|
||||
</div>
|
||||
<div class="flex flex-col gap-7">
|
||||
<h1 class="font-young-serif text-desktop-heading-l text-stone-700 sm:text-stone-800 text-center md:text-left">
|
||||
|
@ -34,7 +34,7 @@
|
||||
<path
|
||||
d="M15 6.954 8.978 9.86a2.25 2.25 0 0 1-1.956 0L1 6.954V11.5A1.5 1.5 0 0 0 2.5 13h11a1.5 1.5 0 0 0 1.5-1.5V6.954Z" />
|
||||
</svg>
|
||||
<input type="email" class="grow" placeholder="Email" id="email" name="email" />
|
||||
<input type="email" class="grow" placeholder="Email" id="email" name="email" value="{{email}}" />
|
||||
</label>
|
||||
|
||||
<label for="password" class="input input-bordered flex items-center gap-2">
|
||||
@ -48,7 +48,7 @@
|
||||
d="M14 6a4 4 0 0 1-4.899 3.899l-1.955 1.955a.5.5 0 0 1-.353.146H5v1.5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-2.293a.5.5 0 0 1 .146-.353l3.955-3.955A4 4 0 1 1 14 6Zm-4-2a.75.75 0 0 0 0 1.5.5.5 0 0 1 .5.5.75.75 0 0 0 1.5 0 2 2 0 0 0-2-2Z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
<input type="password" class="grow" value="password" name="password" id="password" />
|
||||
<input type="password" class="grow" value="" name="password" id="password" />
|
||||
</label>
|
||||
|
||||
<div class="w-full grow">
|
||||
|
@ -1,3 +1,8 @@
|
||||
{% match session %}
|
||||
{% when Some with (user) %}
|
||||
<div class="text-base">Signed in as <span class="text-bold">{{user}}</span></div>
|
||||
{% when None %}
|
||||
<div class="flex justify-center">
|
||||
<a href="/sign-in" class="btn sm:border-2 hover:shadow-lg transition ease-out duration-300">Login</a>
|
||||
</div>
|
||||
{% endmatch %}
|
||||
|
Loading…
Reference in New Issue
Block a user