Working on signed in user
This commit is contained in:
parent
4d0e589a44
commit
9a05b64eaf
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -2638,6 +2638,7 @@ dependencies = [
|
||||
name = "oswilno-view"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"actix-jwt-session",
|
||||
"actix-web",
|
||||
"askama",
|
||||
"askama_actix",
|
||||
|
@ -771,6 +771,10 @@ select {
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.-z-10 {
|
||||
z-index: -10;
|
||||
}
|
||||
|
||||
.z-50 {
|
||||
z-index: 50;
|
||||
}
|
||||
@ -853,6 +857,10 @@ select {
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.h-\[28rem\] {
|
||||
height: 28rem;
|
||||
}
|
||||
|
||||
.h-full {
|
||||
height: 100%;
|
||||
}
|
||||
@ -976,16 +984,6 @@ select {
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.border-gray-100 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(243 244 246 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-gray-200 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(229 231 235 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-gray-300 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(209 213 219 / var(--tw-border-opacity));
|
||||
@ -1021,6 +1019,10 @@ select {
|
||||
background-color: rgb(254 226 226 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-transparent {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.bg-white {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
||||
@ -1310,16 +1312,6 @@ select {
|
||||
background-color: rgb(55 65 81 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-gray-800 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(31 41 55 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-gray-900 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(17 24 39 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-gray-200 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(229 231 235 / var(--tw-text-opacity));
|
||||
@ -1438,11 +1430,6 @@ select {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.md\:bg-white {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.md\:p-0 {
|
||||
padding: 0px;
|
||||
}
|
||||
@ -1462,11 +1449,6 @@ select {
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.md\:dark\:bg-gray-900 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(17 24 39 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.md\:dark\:text-blue-500 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(59 130 246 / var(--tw-text-opacity));
|
||||
|
@ -4,11 +4,13 @@ use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Validati
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
use async_trait::async_trait;
|
||||
|
||||
pub static HEADER_NAME: &str = "Authorization";
|
||||
|
||||
pub trait Claims: PartialEq + DeserializeOwned + Serialize + Clone + Send + Sync + 'static {
|
||||
fn jti(&self) -> uuid::Uuid;
|
||||
fn subject(&self) -> &str;
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, PartialEq)]
|
||||
@ -121,7 +123,7 @@ impl<T: Claims> FromRequest for MaybeAuthenticated<T> {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
#[async_trait(?Send)]
|
||||
pub trait TokenStorage: Send + Sync {
|
||||
type ClaimsType: Claims;
|
||||
|
||||
@ -182,10 +184,54 @@ impl<ClaimsType: Claims> SessionStorage<ClaimsType> {
|
||||
}
|
||||
}
|
||||
|
||||
struct Extractor;
|
||||
#[async_trait(?Send)]
|
||||
pub trait Extractor {
|
||||
async fn extract_jwt<ClaimsType: Claims>(
|
||||
req: &ServiceRequest,
|
||||
jwt_encoding_key: Arc<EncodingKey>,
|
||||
jwt_decoding_key: Arc<DecodingKey>,
|
||||
algorithm: Algorithm,
|
||||
storage: SessionStorage<ClaimsType>,
|
||||
) -> Result<(), Error>;
|
||||
|
||||
impl Extractor {
|
||||
async fn extract_bearer_jwt<ClaimsType: Claims>(
|
||||
fn decode<ClaimsType: Claims>(value: &str,
|
||||
|
||||
jwt_decoding_key: Arc<DecodingKey>,
|
||||
algorithm: Algorithm,
|
||||
|
||||
) -> Result<ClaimsType, Error> {
|
||||
decode::<ClaimsType>(value, &*jwt_decoding_key, &Validation::new(algorithm)).map_err(
|
||||
|_e| {
|
||||
// let error_message = e.to_string();
|
||||
Error::InvalidSession
|
||||
},
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CookieExtractor;
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl Extractor for CookieExtractor {
|
||||
async fn extract_jwt<ClaimsType: Claims>(
|
||||
req: &ServiceRequest,
|
||||
jwt_encoding_key: Arc<EncodingKey>,
|
||||
jwt_decoding_key: Arc<DecodingKey>,
|
||||
algorithm: Algorithm,
|
||||
storage: SessionStorage<ClaimsType>,
|
||||
) -> Result<(), Error> {
|
||||
let Some(cookie) = req.cookie(HEADER_NAME) else {return Ok(())};
|
||||
let value = cookie.value();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HeaderExtractor;
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl Extractor for HeaderExtractor {
|
||||
async fn extract_jwt<ClaimsType: Claims>(
|
||||
req: &ServiceRequest,
|
||||
jwt_encoding_key: Arc<EncodingKey>,
|
||||
jwt_decoding_key: Arc<DecodingKey>,
|
||||
|
@ -112,7 +112,7 @@ where
|
||||
let storage = self.storage.clone();
|
||||
|
||||
async move {
|
||||
Extractor::extract_bearer_jwt(
|
||||
HeaderExtractor::extract_jwt(
|
||||
&req,
|
||||
jwt_encoding_key,
|
||||
jwt_decoding_key,
|
||||
|
@ -7,6 +7,7 @@ mod m20230726_124452_images;
|
||||
mod m20230726_135630_parking_spaces;
|
||||
mod m20230805_000001_add_email;
|
||||
mod m20230809_135630_add_spot;
|
||||
mod m20230810_105100_create_parking_space_locations;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
@ -19,6 +20,7 @@ impl MigratorTrait for Migrator {
|
||||
Box::new(m20230726_135630_parking_spaces::Migration),
|
||||
Box::new(m20230805_000001_add_email::Migration),
|
||||
Box::new(m20230809_135630_add_spot::Migration),
|
||||
Box::new(m20230810_105100_create_parking_space_locations::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -137,6 +137,7 @@ pub enum ParkingSpace {
|
||||
Id,
|
||||
State,
|
||||
Location,
|
||||
LocationId,
|
||||
Spot,
|
||||
AccountId,
|
||||
CreatedAt,
|
||||
|
@ -4,6 +4,7 @@ pub mod prelude;
|
||||
|
||||
pub mod accounts;
|
||||
pub mod images;
|
||||
pub mod parking_space_locations;
|
||||
pub mod parking_space_rents;
|
||||
pub mod parking_spaces;
|
||||
pub mod sea_orm_active_enums;
|
||||
|
@ -28,22 +28,22 @@ pub struct Model {
|
||||
#[actix_admin(primary_key)]
|
||||
pub id: i32,
|
||||
pub state: ParkingSpaceState,
|
||||
pub location: String,
|
||||
pub account_id: i32,
|
||||
pub created_at: DateTime,
|
||||
pub updated_at: DateTime,
|
||||
pub spot: Option<i32>,
|
||||
pub location_id: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
||||
pub enum Column {
|
||||
Id,
|
||||
State,
|
||||
Location,
|
||||
AccountId,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
Spot,
|
||||
LocationId,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)]
|
||||
@ -61,6 +61,7 @@ impl PrimaryKeyTrait for PrimaryKey {
|
||||
#[derive(Copy, Clone, Debug, EnumIter)]
|
||||
pub enum Relation {
|
||||
Accounts,
|
||||
ParkingSpaceLocations,
|
||||
ParkingSpaceRents,
|
||||
}
|
||||
|
||||
@ -70,11 +71,11 @@ impl ColumnTrait for Column {
|
||||
match self {
|
||||
Self::Id => ColumnType::Integer.def(),
|
||||
Self::State => ParkingSpaceState::db_type(),
|
||||
Self::Location => ColumnType::String(None).def(),
|
||||
Self::AccountId => ColumnType::Integer.def(),
|
||||
Self::CreatedAt => ColumnType::DateTime.def(),
|
||||
Self::UpdatedAt => ColumnType::DateTime.def(),
|
||||
Self::Spot => ColumnType::Integer.def().null(),
|
||||
Self::LocationId => ColumnType::Integer.def().null(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -86,6 +87,12 @@ impl RelationTrait for Relation {
|
||||
.from(Column::AccountId)
|
||||
.to(super::accounts::Column::Id)
|
||||
.into(),
|
||||
Self::ParkingSpaceLocations => {
|
||||
Entity::belongs_to(super::parking_space_locations::Entity)
|
||||
.from(Column::LocationId)
|
||||
.to(super::parking_space_locations::Column::Id)
|
||||
.into()
|
||||
}
|
||||
Self::ParkingSpaceRents => Entity::has_many(super::parking_space_rents::Entity).into(),
|
||||
}
|
||||
}
|
||||
@ -97,6 +104,12 @@ impl Related<super::accounts::Entity> for Entity {
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::parking_space_locations::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::ParkingSpaceLocations.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::parking_space_rents::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::ParkingSpaceRents.def()
|
||||
|
@ -2,5 +2,6 @@
|
||||
|
||||
pub use super::accounts::Entity as Accounts;
|
||||
pub use super::images::Entity as Images;
|
||||
pub use super::parking_space_locations::Entity as ParkingSpaceLocations;
|
||||
pub use super::parking_space_rents::Entity as ParkingSpaceRents;
|
||||
pub use super::parking_spaces::Entity as ParkingSpaces;
|
||||
|
@ -1,3 +1,4 @@
|
||||
use actix_web::http::header::ContentType;
|
||||
use actix_web::web::{scope, Data, Form, ServiceConfig};
|
||||
use actix_web::{get, post, HttpRequest, HttpResponse};
|
||||
use askama_actix::Template;
|
||||
@ -6,7 +7,7 @@ use oswilno_contract::accounts;
|
||||
use oswilno_contract::parking_space_rents;
|
||||
use oswilno_contract::parking_spaces;
|
||||
use oswilno_session::{Authenticated, MaybeAuthenticated};
|
||||
use oswilno_view::{is_partial, Layout, Main, MainOpts, SearchOpts, SessionOpts, Blank};
|
||||
use oswilno_view::{is_partial, Blank, Layout, Main, MainOpts, SearchOpts, SessionOpts};
|
||||
use sea_orm::prelude::*;
|
||||
use sea_orm::ActiveValue::{NotSet, Set};
|
||||
use std::collections::HashMap;
|
||||
@ -14,10 +15,10 @@ use std::sync::Arc;
|
||||
|
||||
pub fn mount(config: &mut ServiceConfig) {
|
||||
config.service(root).service(
|
||||
scope("/parking_spaces")
|
||||
scope("/parking-spaces")
|
||||
.service(form_show)
|
||||
.service(all_parking_spaces)
|
||||
.service(all_parial_parking_spaces)
|
||||
.service(create),
|
||||
.service(create)
|
||||
);
|
||||
}
|
||||
|
||||
@ -25,7 +26,7 @@ pub fn mount(config: &mut ServiceConfig) {
|
||||
#[get("/")]
|
||||
async fn root() -> HttpResponse {
|
||||
HttpResponse::SeeOther()
|
||||
.append_header(("Location", "/parking_spaces/all"))
|
||||
.append_header(("Location", "/parking-spaces/all"))
|
||||
.body("")
|
||||
}
|
||||
|
||||
@ -35,6 +36,7 @@ struct AllPartialParkingSpace {
|
||||
parking_space_rents: Vec<parking_space_rents::Model>,
|
||||
parking_space_by_id: HashMap<i32, parking_spaces::Model>,
|
||||
account_by_id: HashMap<i32, accounts::Model>,
|
||||
session: Option<SessionOpts>,
|
||||
}
|
||||
|
||||
#[autometrics]
|
||||
@ -45,15 +47,14 @@ async fn all_parking_spaces(
|
||||
session: MaybeAuthenticated,
|
||||
) -> HttpResponse {
|
||||
let db = db.into_inner();
|
||||
let parking_spaces = load_parking_spaces(db).await;
|
||||
let session = session.into_option().map(Into::into);
|
||||
let mut parking_spaces = load_parking_spaces(db).await;
|
||||
parking_spaces.session = session.clone();
|
||||
let main = Main {
|
||||
body: parking_spaces,
|
||||
title: Blank,
|
||||
opts: MainOpts {
|
||||
session: session.into_option().map(|s| SessionOpts {
|
||||
login: s.subject.to_owned(),
|
||||
profile_image_url: None,
|
||||
}),
|
||||
session,
|
||||
search: Some(SearchOpts {
|
||||
target_url: None,
|
||||
autocomplete: Vec::with_capacity(0),
|
||||
@ -76,15 +77,6 @@ async fn search_parking_spaces() -> HttpResponse {
|
||||
HttpResponse::Ok().body("")
|
||||
}
|
||||
|
||||
#[autometrics]
|
||||
#[get("/all-partial")]
|
||||
async fn all_parial_parking_spaces(
|
||||
db: Data<sea_orm::DatabaseConnection>,
|
||||
) -> AllPartialParkingSpace {
|
||||
let db = db.into_inner();
|
||||
load_parking_spaces(db).await
|
||||
}
|
||||
|
||||
async fn load_parking_spaces(db: Arc<DatabaseConnection>) -> AllPartialParkingSpace {
|
||||
let rents = parking_space_rents::Entity::find().all(&*db).await.unwrap();
|
||||
let (parking_space_by_id, account_ids) = {
|
||||
@ -126,35 +118,61 @@ async fn load_parking_spaces(db: Arc<DatabaseConnection>) -> AllPartialParkingSp
|
||||
account_by_id,
|
||||
parking_space_rents: rents,
|
||||
parking_space_by_id,
|
||||
session: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Template)]
|
||||
#[get("/form")]
|
||||
async fn form_show(req: HttpRequest, session: Authenticated) -> HttpResponse {
|
||||
let session = session.into();
|
||||
let body = ParkingSpaceFormPartial {
|
||||
..Default::default()
|
||||
};
|
||||
let main = Main {
|
||||
body,
|
||||
title: Blank,
|
||||
opts: MainOpts {
|
||||
show: true,
|
||||
search: None,
|
||||
session: Some(session),
|
||||
},
|
||||
};
|
||||
let html = if is_partial(&req) {
|
||||
main.render()
|
||||
} else {
|
||||
Layout { main }.render()
|
||||
};
|
||||
HttpResponse::Ok()
|
||||
.append_header(ContentType::html())
|
||||
.body(html.unwrap())
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Template)]
|
||||
#[template(path = "../templates/parking-spaces/form-partial.html")]
|
||||
struct ParkingSpaceFormPartial {
|
||||
form: CreateParkingSpace,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
#[derive(Debug, Default, serde::Deserialize)]
|
||||
struct CreateParkingSpace {
|
||||
location: String,
|
||||
location_id: u32,
|
||||
spot: Option<u32>,
|
||||
}
|
||||
|
||||
#[autometrics]
|
||||
#[post("/parking-spaces")]
|
||||
#[post("/create")]
|
||||
async fn create(
|
||||
db: Data<sea_orm::DatabaseConnection>,
|
||||
p: Form<CreateParkingSpace>,
|
||||
session: Authenticated,
|
||||
) -> HttpResponse {
|
||||
use oswilno_contract::parking_spaces::*;
|
||||
let CreateParkingSpace { location, spot } = p.into_inner();
|
||||
let CreateParkingSpace { location_id, spot } = p.into_inner();
|
||||
let db = db.into_inner();
|
||||
|
||||
let model = ActiveModel {
|
||||
id: NotSet,
|
||||
location: Set(location.clone()),
|
||||
location_id: Set(Some(location_id.clone() as i32)),
|
||||
spot: Set(spot.map(|n| n as i32)),
|
||||
account_id: Set(session.account_id()),
|
||||
..Default::default()
|
||||
@ -162,7 +180,7 @@ async fn create(
|
||||
if let Err(_e) = model.save(&*db).await {
|
||||
return HttpResponse::BadRequest().body(
|
||||
ParkingSpaceFormPartial {
|
||||
form: CreateParkingSpace { location, spot },
|
||||
form: CreateParkingSpace { location_id, spot },
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
|
@ -8,11 +8,20 @@
|
||||
</div>
|
||||
<div>
|
||||
</div>
|
||||
{% match session %}
|
||||
{% when Some with (session) %}
|
||||
<div>
|
||||
<button class="text-emerald-500 background-transparent font-bold uppercase px-6 py-2 text-sm outline-none focus:outline-none mr-1 mb-1 ease-linear transition-all duration-150 border-2 rounded-full">
|
||||
<a
|
||||
href="/parking-spaces/form"
|
||||
x-get="/parking-spaces/form"
|
||||
x-target="main"
|
||||
class="text-emerald-500 background-transparent font-bold uppercase px-6 py-2 text-sm outline-none focus:outline-none mr-1 mb-1 ease-linear transition-all duration-150 border-2 rounded-full"
|
||||
>
|
||||
<i class="fas fa-plus"></i>
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
{% when None %}
|
||||
{% endmatch %}
|
||||
</section>
|
||||
<oswilno-parking-space-rents>
|
||||
{% for parking_space_rent in parking_space_rents -%}
|
||||
|
@ -9,7 +9,7 @@ use askama_actix::Template;
|
||||
use autometrics::autometrics;
|
||||
use garde::Validate;
|
||||
use jsonwebtoken::*;
|
||||
use oswilno_view::{Errors, Lang, Layout, Main, MainOpts, TranslationStorage, Blank};
|
||||
use oswilno_view::{Blank, Errors, Lang, Layout, Main, MainOpts, TranslationStorage};
|
||||
use ring::rand::SystemRandom;
|
||||
use ring::signature::{Ed25519KeyPair, KeyPair};
|
||||
use sea_orm::DatabaseConnection;
|
||||
@ -54,6 +54,10 @@ impl actix_jwt_session::Claims for Claims {
|
||||
fn jti(&self) -> uuid::Uuid {
|
||||
self.jwt_id
|
||||
}
|
||||
|
||||
fn subject(&self) -> &str {
|
||||
&self.subject
|
||||
}
|
||||
}
|
||||
|
||||
impl Claims {
|
||||
@ -290,11 +294,14 @@ async fn login_inner(
|
||||
return Err(payload);
|
||||
}
|
||||
};
|
||||
|
||||
let cookie = actix_web::cookie::Cookie::build(actix_jwt_session::HEADER_NAME, &bearer_token).http_only(true).finish();
|
||||
Ok(HttpResponse::Ok()
|
||||
.append_header((
|
||||
actix_jwt_session::HEADER_NAME,
|
||||
format!("Bearer {bearer_token}").as_str(),
|
||||
))
|
||||
.cookie(cookie)
|
||||
.body(""))
|
||||
}
|
||||
|
||||
|
@ -10,3 +10,4 @@ askama_actix = { version = "0.14.0" }
|
||||
futures-core = "0.3.28"
|
||||
garde = { version = "0.14.0", features = ["derive"] }
|
||||
tracing = "0.1.37"
|
||||
actix-jwt-session = { path = "../actix-jwt-session" }
|
||||
|
@ -34,12 +34,20 @@ pub struct SearchOpts {
|
||||
pub autocomplete: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct SessionOpts {
|
||||
pub login: String,
|
||||
pub profile_image_url: Option<String>,
|
||||
}
|
||||
|
||||
impl<C: actix_jwt_session::Claims> From<actix_jwt_session::Authenticated<C>> for SessionOpts {
|
||||
fn from(session: actix_jwt_session::Authenticated<C>) -> Self {
|
||||
Self {
|
||||
login: session.subject().to_owned(),
|
||||
profile_image_url: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub struct MainOpts {
|
||||
pub show: bool,
|
||||
|
@ -1,7 +1,7 @@
|
||||
<main>
|
||||
{% include "navbar.html" %}
|
||||
<section id="main-header" class="relative pt-16 pb-32 flex content-center items-center justify-center" style="min-height:75vh">
|
||||
<div class="absolute top-0 w-full h-full bg-center bg-cover" style="background-image:url('/banner.jpg')">
|
||||
<section id="main-header" class="pt-16 pb-32 flex content-center items-center justify-center">
|
||||
<div class="absolute -z-10 top-0 w-full h-[28rem] bg-center bg-cover" style="background-image:url('/banner.jpg')">
|
||||
<span id="blackOverlay" class="w-full h-full absolute opacity-75 bg-black"></span>
|
||||
</div>
|
||||
{{ title|safe }}
|
||||
|
@ -1,5 +1,5 @@
|
||||
<div class="items-center justify-between hidden w-full md:flex md:w-auto md:order-1" id="navbar-user">
|
||||
<ul class="flex flex-col font-medium p-4 md:p-0 mt-4 border border-gray-100 rounded-lg bg-gray-50 md:flex-row md:space-x-8 md:mt-0 md:border-0 md:bg-white dark:bg-gray-800 md:dark:bg-gray-900 dark:border-gray-700">
|
||||
<ul class="flex flex-col font-medium p-4 md:p-0 mt-4 border rounded-lg md:flex-row md:space-x-8 md:mt-0 md:border-0">
|
||||
<li>
|
||||
<a
|
||||
href="/"
|
||||
|
@ -1,4 +1,4 @@
|
||||
<nav class="bg-white border-gray-200 dark:bg-gray-900">
|
||||
<nav class="bg-transparent">
|
||||
<div class="max-w-screen-xl flex flex-wrap items-center justify-between mx-auto p-4">
|
||||
<a href="https://flowbite.com/" class="flex items-center">
|
||||
<img src="https://flowbite.com/docs/images/logo.svg" class="h-8 mr-3" alt="Flowbite Logo" />
|
||||
|
Loading…
Reference in New Issue
Block a user