Compare commits
	
		
			2 commits
		
	
	
		
			331423d3f6
			...
			22ad0a0a11
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 22ad0a0a11 | |||
| 18251d7f00 | 
					 42 changed files with 1981 additions and 1036 deletions
				
			
		| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
{
 | 
			
		||||
  "db_name": "PostgreSQL",
 | 
			
		||||
  "query": "SELECT id, title FROM paste",
 | 
			
		||||
  "query": "SELECT id, title, content FROM paste",
 | 
			
		||||
  "describe": {
 | 
			
		||||
    "columns": [
 | 
			
		||||
      {
 | 
			
		||||
| 
						 | 
				
			
			@ -12,15 +12,21 @@
 | 
			
		|||
        "ordinal": 1,
 | 
			
		||||
        "name": "title",
 | 
			
		||||
        "type_info": "Text"
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "ordinal": 2,
 | 
			
		||||
        "name": "content",
 | 
			
		||||
        "type_info": "Text"
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "parameters": {
 | 
			
		||||
      "Left": []
 | 
			
		||||
    },
 | 
			
		||||
    "nullable": [
 | 
			
		||||
      false,
 | 
			
		||||
      false,
 | 
			
		||||
      false
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "hash": "218cab8328020896b78a81be345a2074ca5461affae6b7f4daa07ba6618f96aa"
 | 
			
		||||
  "hash": "118b5224bd11d8a0e9ab6e2c3b44ec4fc98990bd7fe434966fcd2e3580901f70"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										14
									
								
								.sqlx/query-8f568c34f779acebeb3ddec7193812043f01e718143e028058614bfc1c9265c9.json
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								.sqlx/query-8f568c34f779acebeb3ddec7193812043f01e718143e028058614bfc1c9265c9.json
									
										
									
										generated
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,14 @@
 | 
			
		|||
{
 | 
			
		||||
  "db_name": "PostgreSQL",
 | 
			
		||||
  "query": "DELETE FROM paste WHERE id = $1",
 | 
			
		||||
  "describe": {
 | 
			
		||||
    "columns": [],
 | 
			
		||||
    "parameters": {
 | 
			
		||||
      "Left": [
 | 
			
		||||
        "Uuid"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "nullable": []
 | 
			
		||||
  },
 | 
			
		||||
  "hash": "8f568c34f779acebeb3ddec7193812043f01e718143e028058614bfc1c9265c9"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										35
									
								
								.sqlx/query-a1945bd23b49ad7bf7aab90f624c7f9d71756f1ad47ce4d8a66fa77c83bd6fdc.json
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								.sqlx/query-a1945bd23b49ad7bf7aab90f624c7f9d71756f1ad47ce4d8a66fa77c83bd6fdc.json
									
										
									
										generated
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,35 @@
 | 
			
		|||
{
 | 
			
		||||
  "db_name": "PostgreSQL",
 | 
			
		||||
  "query": "SELECT id, title, content from paste LIMIT $1 OFFSET $2",
 | 
			
		||||
  "describe": {
 | 
			
		||||
    "columns": [
 | 
			
		||||
      {
 | 
			
		||||
        "ordinal": 0,
 | 
			
		||||
        "name": "id",
 | 
			
		||||
        "type_info": "Uuid"
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "ordinal": 1,
 | 
			
		||||
        "name": "title",
 | 
			
		||||
        "type_info": "Text"
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "ordinal": 2,
 | 
			
		||||
        "name": "content",
 | 
			
		||||
        "type_info": "Text"
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "parameters": {
 | 
			
		||||
      "Left": [
 | 
			
		||||
        "Int8",
 | 
			
		||||
        "Int8"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "nullable": [
 | 
			
		||||
      false,
 | 
			
		||||
      false,
 | 
			
		||||
      false
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "hash": "a1945bd23b49ad7bf7aab90f624c7f9d71756f1ad47ce4d8a66fa77c83bd6fdc"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										16
									
								
								.sqlx/query-b3a91e683e3a0edd655140eaf788e7ad18447b27c87c9009f8d14e2a9046a3b4.json
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								.sqlx/query-b3a91e683e3a0edd655140eaf788e7ad18447b27c87c9009f8d14e2a9046a3b4.json
									
										
									
										generated
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,16 @@
 | 
			
		|||
{
 | 
			
		||||
  "db_name": "PostgreSQL",
 | 
			
		||||
  "query": "INSERT INTO paste (id, title, content) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING",
 | 
			
		||||
  "describe": {
 | 
			
		||||
    "columns": [],
 | 
			
		||||
    "parameters": {
 | 
			
		||||
      "Left": [
 | 
			
		||||
        "Uuid",
 | 
			
		||||
        "Text",
 | 
			
		||||
        "Text"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "nullable": []
 | 
			
		||||
  },
 | 
			
		||||
  "hash": "b3a91e683e3a0edd655140eaf788e7ad18447b27c87c9009f8d14e2a9046a3b4"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										16
									
								
								.sqlx/query-c1e8d4b7bc7e5e088a7a88bf0dcf6a19ac454a4d2b79ed8aa3f79e507ca55f41.json
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								.sqlx/query-c1e8d4b7bc7e5e088a7a88bf0dcf6a19ac454a4d2b79ed8aa3f79e507ca55f41.json
									
										
									
										generated
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,16 @@
 | 
			
		|||
{
 | 
			
		||||
  "db_name": "PostgreSQL",
 | 
			
		||||
  "query": "UPDATE paste SET title = $2, content = $3 WHERE id = $1",
 | 
			
		||||
  "describe": {
 | 
			
		||||
    "columns": [],
 | 
			
		||||
    "parameters": {
 | 
			
		||||
      "Left": [
 | 
			
		||||
        "Uuid",
 | 
			
		||||
        "Text",
 | 
			
		||||
        "Text"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "nullable": []
 | 
			
		||||
  },
 | 
			
		||||
  "hash": "c1e8d4b7bc7e5e088a7a88bf0dcf6a19ac454a4d2b79ed8aa3f79e507ca55f41"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										22
									
								
								.sqlx/query-ea3d1dd65be91a19d3ed1570cabb733103086502d2a2c6dd696cd97493157ed7.json
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								.sqlx/query-ea3d1dd65be91a19d3ed1570cabb733103086502d2a2c6dd696cd97493157ed7.json
									
										
									
										generated
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,22 @@
 | 
			
		|||
{
 | 
			
		||||
  "db_name": "PostgreSQL",
 | 
			
		||||
  "query": "SELECT content FROM paste WHERE id = $1",
 | 
			
		||||
  "describe": {
 | 
			
		||||
    "columns": [
 | 
			
		||||
      {
 | 
			
		||||
        "ordinal": 0,
 | 
			
		||||
        "name": "content",
 | 
			
		||||
        "type_info": "Text"
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "parameters": {
 | 
			
		||||
      "Left": [
 | 
			
		||||
        "Uuid"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "nullable": [
 | 
			
		||||
      false
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "hash": "ea3d1dd65be91a19d3ed1570cabb733103086502d2a2c6dd696cd97493157ed7"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										616
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										616
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										29
									
								
								Cargo.toml
									
										
									
									
									
								
							
							
						
						
									
										29
									
								
								Cargo.toml
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -4,24 +4,31 @@ version = "0.1.0"
 | 
			
		|||
edition = "2021"
 | 
			
		||||
 | 
			
		||||
[dependencies]
 | 
			
		||||
axum = { version = "0.7.5", default-features = false, features = ["http1", "json", "macros", "matched-path", "tokio", "tower-log", "tracing", "query", "form"] }
 | 
			
		||||
axum-extra = { version = "0.9.3", features = ["async-read-body"] }
 | 
			
		||||
askama = "0.12.1"
 | 
			
		||||
askama_axum = "0.4.0"
 | 
			
		||||
async-trait = "0.1.83"
 | 
			
		||||
axum = { version = "0.7.6", default-features = false, features = ["http1", "json", "macros", "matched-path", "tokio", "tower-log", "tracing", "query", "form"] }
 | 
			
		||||
axum-codec = { version = "0.0.13", features = ["cbor"] }
 | 
			
		||||
axum-core = "0.4.5"
 | 
			
		||||
axum-extra = { version = "0.9.4", features = ["async-read-body"] }
 | 
			
		||||
bytes = "1.7.2"
 | 
			
		||||
chrono = { version = "0.4.38", default-features = false, features = ["alloc", "clock"] }
 | 
			
		||||
eyre = "0.6.12"
 | 
			
		||||
figment = { version = "0.10.19", features = ["env", "toml"] }
 | 
			
		||||
http = "1.1.0"
 | 
			
		||||
maud = { version = "0.26.0", features = ["axum"] }
 | 
			
		||||
memory-serve = "0.4.5"
 | 
			
		||||
serde = { version = "1.0.203", features = ["derive"] }
 | 
			
		||||
sqlx = { version = "0.7.4", features = ["runtime-tokio", "postgres", "uuid"] }
 | 
			
		||||
thiserror = "1.0.61"
 | 
			
		||||
tokio = { version = "1.38.0", features = ["rt-multi-thread", "macros", "fs", "io-std"] }
 | 
			
		||||
tower-http = { version = "0.5.2", features = ["trace", "fs"] }
 | 
			
		||||
memory-serve = "0.6.0"
 | 
			
		||||
mime = "0.3.17"
 | 
			
		||||
serde = { version = "1.0.210", features = ["derive"] }
 | 
			
		||||
sqlx = { version = "0.8.2", features = ["runtime-tokio", "postgres", "uuid"] }
 | 
			
		||||
thiserror = "1.0.64"
 | 
			
		||||
tokio = { version = "1.40.0", features = ["rt-multi-thread", "macros", "fs", "io-std"] }
 | 
			
		||||
tower-http = { version = "0.6.1", features = ["trace", "fs"] }
 | 
			
		||||
tracing = "0.1.40"
 | 
			
		||||
tracing-subscriber = "0.3.18"
 | 
			
		||||
ulid = { version = "1.1.2", features = ["uuid", "serde"] }
 | 
			
		||||
url = { version = "2.5.0", features = ["serde"] }
 | 
			
		||||
uuid = "1.8.0"
 | 
			
		||||
ulid = { version = "1.1.3", features = ["uuid", "serde"] }
 | 
			
		||||
url = { version = "2.5.2", features = ["serde"] }
 | 
			
		||||
uuid = "1.10.0"
 | 
			
		||||
 | 
			
		||||
[profile.release]
 | 
			
		||||
strip = true
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										2
									
								
								askama.toml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								askama.toml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,2 @@
 | 
			
		|||
[general]
 | 
			
		||||
whitespace = "minimize"
 | 
			
		||||
							
								
								
									
										24
									
								
								flake.lock
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										24
									
								
								flake.lock
									
										
									
										generated
									
									
									
								
							| 
						 | 
				
			
			@ -8,11 +8,11 @@
 | 
			
		|||
        "rust-analyzer-src": "rust-analyzer-src"
 | 
			
		||||
      },
 | 
			
		||||
      "locked": {
 | 
			
		||||
        "lastModified": 1717741716,
 | 
			
		||||
        "narHash": "sha256-v71DDu2gb02iBIAhUwu5mQZNtYD45QcbEqahqsOCCyU=",
 | 
			
		||||
        "lastModified": 1727159616,
 | 
			
		||||
        "narHash": "sha256-1VjZ+khJwZphRJZy2HvbMSCgi3OV7mu8RjVzqCxVi2k=",
 | 
			
		||||
        "owner": "nix-community",
 | 
			
		||||
        "repo": "fenix",
 | 
			
		||||
        "rev": "05f2a8ae62c45637b20d162fa2dd450b79e71c27",
 | 
			
		||||
        "rev": "4306d494985e00719573bbdeb863c27c6d83dc9c",
 | 
			
		||||
        "type": "github"
 | 
			
		||||
      },
 | 
			
		||||
      "original": {
 | 
			
		||||
| 
						 | 
				
			
			@ -26,11 +26,11 @@
 | 
			
		|||
        "systems": "systems"
 | 
			
		||||
      },
 | 
			
		||||
      "locked": {
 | 
			
		||||
        "lastModified": 1710146030,
 | 
			
		||||
        "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
 | 
			
		||||
        "lastModified": 1726560853,
 | 
			
		||||
        "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
 | 
			
		||||
        "owner": "numtide",
 | 
			
		||||
        "repo": "flake-utils",
 | 
			
		||||
        "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
 | 
			
		||||
        "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
 | 
			
		||||
        "type": "github"
 | 
			
		||||
      },
 | 
			
		||||
      "original": {
 | 
			
		||||
| 
						 | 
				
			
			@ -41,11 +41,11 @@
 | 
			
		|||
    },
 | 
			
		||||
    "nixpkgs": {
 | 
			
		||||
      "locked": {
 | 
			
		||||
        "lastModified": 1717602782,
 | 
			
		||||
        "narHash": "sha256-pL9jeus5QpX5R+9rsp3hhZ+uplVHscNJh8n8VpqscM0=",
 | 
			
		||||
        "lastModified": 1726937504,
 | 
			
		||||
        "narHash": "sha256-bvGoiQBvponpZh8ClUcmJ6QnsNKw0EMrCQJARK3bI1c=",
 | 
			
		||||
        "owner": "NixOS",
 | 
			
		||||
        "repo": "nixpkgs",
 | 
			
		||||
        "rev": "e8057b67ebf307f01bdcc8fba94d94f75039d1f6",
 | 
			
		||||
        "rev": "9357f4f23713673f310988025d9dc261c20e70c6",
 | 
			
		||||
        "type": "github"
 | 
			
		||||
      },
 | 
			
		||||
      "original": {
 | 
			
		||||
| 
						 | 
				
			
			@ -65,11 +65,11 @@
 | 
			
		|||
    "rust-analyzer-src": {
 | 
			
		||||
      "flake": false,
 | 
			
		||||
      "locked": {
 | 
			
		||||
        "lastModified": 1717583671,
 | 
			
		||||
        "narHash": "sha256-+lRAmz92CNUxorqWusgJbL9VE1eKCnQQojglRemzwkw=",
 | 
			
		||||
        "lastModified": 1727104575,
 | 
			
		||||
        "narHash": "sha256-lB/ZS0SnHyE8Z3G8DIL/QJPg6w6x5ZhgVO2pBqnz89g=",
 | 
			
		||||
        "owner": "rust-lang",
 | 
			
		||||
        "repo": "rust-analyzer",
 | 
			
		||||
        "rev": "48bbdd6a74f3176987d5c809894ac33957000d19",
 | 
			
		||||
        "rev": "3d0343251fe084b335b55c17a52bb4a3527b1bd0",
 | 
			
		||||
        "type": "github"
 | 
			
		||||
      },
 | 
			
		||||
      "original": {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -44,7 +44,7 @@
 | 
			
		|||
            postgresql
 | 
			
		||||
            sqlfluff
 | 
			
		||||
            sqlx-cli
 | 
			
		||||
            tailwindcss
 | 
			
		||||
            dart-sass
 | 
			
		||||
          ];
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,38 +15,11 @@ use crate::{app::SharedState, error::AppError};
 | 
			
		|||
pub fn resource() -> Resource<SharedState> {
 | 
			
		||||
    Resource::named("links")
 | 
			
		||||
        .create(create_link)
 | 
			
		||||
        .show(get_link_info)
 | 
			
		||||
        .show(show_link)
 | 
			
		||||
        .update(update_link)
 | 
			
		||||
        .destroy(delete_link)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize)]
 | 
			
		||||
struct Link {
 | 
			
		||||
    id: Ulid,
 | 
			
		||||
    slug: String,
 | 
			
		||||
    destination: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn get_link_info(
 | 
			
		||||
    State(SharedState { db, .. }): State<SharedState>,
 | 
			
		||||
    Path(id): Path<Ulid>,
 | 
			
		||||
) -> Result<Json<Link>, AppError> {
 | 
			
		||||
    match query!(
 | 
			
		||||
        "SELECT id, slug, destination FROM link WHERE id = $1",
 | 
			
		||||
        Uuid::from(id),
 | 
			
		||||
    )
 | 
			
		||||
    .fetch_optional(&db)
 | 
			
		||||
    .await?
 | 
			
		||||
    {
 | 
			
		||||
        Some(r) => Ok(Json(Link {
 | 
			
		||||
            id: Ulid::from(r.id),
 | 
			
		||||
            slug: r.slug,
 | 
			
		||||
            destination: r.destination,
 | 
			
		||||
        })),
 | 
			
		||||
        None => Err(AppError::LinkNotFoundId(id)),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Deserialize)]
 | 
			
		||||
struct CreateLinkRequestBody {
 | 
			
		||||
    slug: String,
 | 
			
		||||
| 
						 | 
				
			
			@ -79,6 +52,33 @@ async fn create_link(
 | 
			
		|||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize)]
 | 
			
		||||
struct Link {
 | 
			
		||||
    id: Ulid,
 | 
			
		||||
    slug: String,
 | 
			
		||||
    destination: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn show_link(
 | 
			
		||||
    State(SharedState { db, .. }): State<SharedState>,
 | 
			
		||||
    Path(id): Path<Ulid>,
 | 
			
		||||
) -> Result<Json<Link>, AppError> {
 | 
			
		||||
    match query!(
 | 
			
		||||
        "SELECT id, slug, destination FROM link WHERE id = $1",
 | 
			
		||||
        Uuid::from(id),
 | 
			
		||||
    )
 | 
			
		||||
    .fetch_optional(&db)
 | 
			
		||||
    .await?
 | 
			
		||||
    {
 | 
			
		||||
        Some(r) => Ok(Json(Link {
 | 
			
		||||
            id: Ulid::from(r.id),
 | 
			
		||||
            slug: r.slug,
 | 
			
		||||
            destination: r.destination,
 | 
			
		||||
        })),
 | 
			
		||||
        None => Err(AppError::LinkNotFoundId(id)),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Deserialize)]
 | 
			
		||||
struct UpdateLinkRequestBody {
 | 
			
		||||
    destination: Url,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,9 +1,21 @@
 | 
			
		|||
mod links;
 | 
			
		||||
mod pastes;
 | 
			
		||||
 | 
			
		||||
use axum::Router;
 | 
			
		||||
use serde::Deserialize;
 | 
			
		||||
 | 
			
		||||
use super::SharedState;
 | 
			
		||||
 | 
			
		||||
pub fn router() -> Router<SharedState> {
 | 
			
		||||
    Router::new().merge(links::resource())
 | 
			
		||||
    Router::new()
 | 
			
		||||
        .merge(links::resource())
 | 
			
		||||
        .merge(pastes::resource())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Deserialize)]
 | 
			
		||||
struct Pagination {
 | 
			
		||||
    #[serde(default)]
 | 
			
		||||
    limit: Option<i64>,
 | 
			
		||||
    #[serde(default)]
 | 
			
		||||
    offset: i64,
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										144
									
								
								src/app/api/pastes.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								src/app/api/pastes.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,144 @@
 | 
			
		|||
use axum::{
 | 
			
		||||
    extract::{Path, Query, State},
 | 
			
		||||
    Json,
 | 
			
		||||
};
 | 
			
		||||
use axum_codec::Codec;
 | 
			
		||||
use http::StatusCode;
 | 
			
		||||
use serde::Deserialize;
 | 
			
		||||
use sqlx::query;
 | 
			
		||||
use ulid::Ulid;
 | 
			
		||||
use uuid::Uuid;
 | 
			
		||||
 | 
			
		||||
use super::Pagination;
 | 
			
		||||
use crate::{
 | 
			
		||||
    app::{resource::Resource, SharedState},
 | 
			
		||||
    error::AppError,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
pub fn resource() -> Resource<SharedState> {
 | 
			
		||||
    Resource::named("pastes")
 | 
			
		||||
        .index(list_pastes)
 | 
			
		||||
        .create(create_paste)
 | 
			
		||||
        .show(show_paste)
 | 
			
		||||
        .update(update_paste)
 | 
			
		||||
        .destroy(delete_paste)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[axum_codec::apply(encode)]
 | 
			
		||||
struct Paste {
 | 
			
		||||
    id: Ulid,
 | 
			
		||||
    title: String,
 | 
			
		||||
    content: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn list_pastes(
 | 
			
		||||
    State(SharedState { db, .. }): State<SharedState>,
 | 
			
		||||
    Query(Pagination { limit, offset }): Query<Pagination>,
 | 
			
		||||
) -> Result<Codec<Vec<Paste>>, AppError> {
 | 
			
		||||
    Ok(Codec(
 | 
			
		||||
        query!(
 | 
			
		||||
            "SELECT id, title, content from paste LIMIT $1 OFFSET $2",
 | 
			
		||||
            limit,
 | 
			
		||||
            offset,
 | 
			
		||||
        )
 | 
			
		||||
        .fetch_all(&db)
 | 
			
		||||
        .await?
 | 
			
		||||
        .into_iter()
 | 
			
		||||
        .map(|r| Paste {
 | 
			
		||||
            id: Ulid::from(r.id),
 | 
			
		||||
            title: r.title,
 | 
			
		||||
            content: r.content,
 | 
			
		||||
        })
 | 
			
		||||
        .collect(),
 | 
			
		||||
    ))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[axum_codec::apply(decode)]
 | 
			
		||||
struct CreatePasteRequestBody {
 | 
			
		||||
    title: String,
 | 
			
		||||
    content: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn create_paste(
 | 
			
		||||
    State(SharedState { db, .. }): State<SharedState>,
 | 
			
		||||
    Codec(CreatePasteRequestBody { title, content }): Codec<CreatePasteRequestBody>,
 | 
			
		||||
) -> Result<Codec<Paste>, AppError> {
 | 
			
		||||
    let id = Ulid::new();
 | 
			
		||||
 | 
			
		||||
    match query!(
 | 
			
		||||
        "INSERT INTO paste (id, title, content) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING",
 | 
			
		||||
        Uuid::from(id),
 | 
			
		||||
        title,
 | 
			
		||||
        content,
 | 
			
		||||
    )
 | 
			
		||||
    .execute(&db)
 | 
			
		||||
    .await?
 | 
			
		||||
    .rows_affected()
 | 
			
		||||
    {
 | 
			
		||||
        1 => Ok(Codec(Paste { id, title, content })),
 | 
			
		||||
        0 => Err(AppError::UlidConflict(id)),
 | 
			
		||||
        rows => Err(AppError::ImpossibleAffectedRows(rows)),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn show_paste(
 | 
			
		||||
    State(SharedState { db, .. }): State<SharedState>,
 | 
			
		||||
    Path(id): Path<Ulid>,
 | 
			
		||||
) -> Result<Codec<Paste>, AppError> {
 | 
			
		||||
    match query!(
 | 
			
		||||
        "SELECT title, content FROM paste WHERE id = $1",
 | 
			
		||||
        Uuid::from(id),
 | 
			
		||||
    )
 | 
			
		||||
    .fetch_optional(&db)
 | 
			
		||||
    .await?
 | 
			
		||||
    {
 | 
			
		||||
        Some(r) => Ok(Codec(Paste {
 | 
			
		||||
            id,
 | 
			
		||||
            title: r.title,
 | 
			
		||||
            content: r.content,
 | 
			
		||||
        })),
 | 
			
		||||
        None => Err(AppError::PasteNotFound(id)),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Deserialize)]
 | 
			
		||||
struct UpdatePasteRequestBody {
 | 
			
		||||
    title: String,
 | 
			
		||||
    content: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn update_paste(
 | 
			
		||||
    State(SharedState { db, .. }): State<SharedState>,
 | 
			
		||||
    Path(id): Path<Ulid>,
 | 
			
		||||
    Json(UpdatePasteRequestBody { title, content }): Json<UpdatePasteRequestBody>,
 | 
			
		||||
) -> Result<StatusCode, AppError> {
 | 
			
		||||
    match query!(
 | 
			
		||||
        "UPDATE paste SET title = $2, content = $3 WHERE id = $1",
 | 
			
		||||
        Uuid::from(id),
 | 
			
		||||
        title,
 | 
			
		||||
        content,
 | 
			
		||||
    )
 | 
			
		||||
    .execute(&db)
 | 
			
		||||
    .await?
 | 
			
		||||
    .rows_affected()
 | 
			
		||||
    {
 | 
			
		||||
        1 => Ok(StatusCode::NO_CONTENT),
 | 
			
		||||
        0 => Err(AppError::PasteNotFound(id)),
 | 
			
		||||
        rows => Err(AppError::ImpossibleAffectedRows(rows)),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn delete_paste(
 | 
			
		||||
    State(SharedState { db, .. }): State<SharedState>,
 | 
			
		||||
    Path(id): Path<Ulid>,
 | 
			
		||||
) -> Result<StatusCode, AppError> {
 | 
			
		||||
    match query!("DELETE FROM paste WHERE id = $1", Uuid::from(id))
 | 
			
		||||
        .execute(&db)
 | 
			
		||||
        .await?
 | 
			
		||||
        .rows_affected()
 | 
			
		||||
    {
 | 
			
		||||
        1 => Ok(StatusCode::NO_CONTENT),
 | 
			
		||||
        0 => Err(AppError::PasteNotFound(id)),
 | 
			
		||||
        rows => Err(AppError::ImpossibleAffectedRows(rows)),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
mod api;
 | 
			
		||||
mod pages;
 | 
			
		||||
mod resource;
 | 
			
		||||
mod root;
 | 
			
		||||
 | 
			
		||||
use std::sync::Arc;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,12 +1,14 @@
 | 
			
		|||
use askama::Template;
 | 
			
		||||
use axum::{
 | 
			
		||||
    extract::{Query, State},
 | 
			
		||||
    response::Redirect,
 | 
			
		||||
    routing::get,
 | 
			
		||||
    Form, Router,
 | 
			
		||||
};
 | 
			
		||||
use chrono::{DateTime, TimeZone, Utc};
 | 
			
		||||
use maud::{html, Markup};
 | 
			
		||||
use serde::Deserialize;
 | 
			
		||||
use sqlx::query;
 | 
			
		||||
use sqlx::{query, query_as};
 | 
			
		||||
use ulid::Ulid;
 | 
			
		||||
use uuid::Uuid;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -27,38 +29,78 @@ pub(super) fn router() -> Router<SharedState> {
 | 
			
		|||
 | 
			
		||||
const BC_PASTES: Breadcrumb = Breadcrumb::new_static("Pastes", "/admin/pastes");
 | 
			
		||||
 | 
			
		||||
// async fn show_pastes(
 | 
			
		||||
//     State(SharedState { db, .. }): State<SharedState>,
 | 
			
		||||
// ) -> Result<Markup, AppError> {
 | 
			
		||||
//     Ok(page(
 | 
			
		||||
//         "Pastes",
 | 
			
		||||
//         &[BC_INDEX, BC_ADMIN],
 | 
			
		||||
//         html! {
 | 
			
		||||
//             table class="w-full" {
 | 
			
		||||
//                 thead {
 | 
			
		||||
//                     tr {
 | 
			
		||||
//                         th { "Name" }
 | 
			
		||||
//                         th { "Created at" }
 | 
			
		||||
//                         th { "Actions" }
 | 
			
		||||
//                     }
 | 
			
		||||
//                 }
 | 
			
		||||
//                 tbody {
 | 
			
		||||
//                     @for p in query!("SELECT id, title FROM
 | 
			
		||||
// paste").fetch_all(&db).await? {                         tr {
 | 
			
		||||
//                             td { a.tl href=(format!("/p/{}",
 | 
			
		||||
// Ulid::from(p.id))) { (p.title)} }                             td {
 | 
			
		||||
// (dt_iso(Ulid::from(p.id).datetime())) }                             td
 | 
			
		||||
// class="w-min-content" {                                 div class="flex
 | 
			
		||||
// gap-2" {                                     a.tl href="#" { "Edit" }
 | 
			
		||||
//                                     a.tl href="#" { "Delete" }
 | 
			
		||||
//                                 }
 | 
			
		||||
//                             }
 | 
			
		||||
//                         }
 | 
			
		||||
//                     }
 | 
			
		||||
//                 }
 | 
			
		||||
//             }
 | 
			
		||||
//         },
 | 
			
		||||
//     ))
 | 
			
		||||
// }
 | 
			
		||||
 | 
			
		||||
struct PasteInList {
 | 
			
		||||
    id: Ulid,
 | 
			
		||||
    title: String,
 | 
			
		||||
    content: String,
 | 
			
		||||
    timestamp: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Template)]
 | 
			
		||||
#[template(path = "admin/pastes.html")]
 | 
			
		||||
struct ShowPastesTemplate {
 | 
			
		||||
    pastes: Vec<PasteInList>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn show_pastes(
 | 
			
		||||
    State(SharedState { db, .. }): State<SharedState>,
 | 
			
		||||
) -> Result<Markup, AppError> {
 | 
			
		||||
    Ok(page(
 | 
			
		||||
        "Pastes",
 | 
			
		||||
        &[BC_INDEX, BC_ADMIN],
 | 
			
		||||
        html! {
 | 
			
		||||
            table class="w-full" {
 | 
			
		||||
                thead {
 | 
			
		||||
                    tr {
 | 
			
		||||
                        th { "Name" }
 | 
			
		||||
                        th { "Created at" }
 | 
			
		||||
                        th { "Actions" }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                tbody {
 | 
			
		||||
                    @for p in query!("SELECT id, title FROM paste").fetch_all(&db).await? {
 | 
			
		||||
                        tr {
 | 
			
		||||
                            td { a.tl href=(format!("/p/{}", Ulid::from(p.id))) { (p.title)} }
 | 
			
		||||
                            td { (dt_iso(Ulid::from(p.id).datetime())) }
 | 
			
		||||
                            td class="w-min-content" {
 | 
			
		||||
                                div class="flex gap-2" {
 | 
			
		||||
                                    a.tl href="#" { "Edit" }
 | 
			
		||||
                                    a.tl href="#" { "Delete" }
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
    ))
 | 
			
		||||
) -> Result<ShowPastesTemplate, AppError> {
 | 
			
		||||
    let pastes = query!("SELECT id, title, content FROM paste")
 | 
			
		||||
        .fetch_all(&db)
 | 
			
		||||
        .await?;
 | 
			
		||||
    let pastes = pastes
 | 
			
		||||
        .into_iter()
 | 
			
		||||
        .map(|r| PasteInList {
 | 
			
		||||
            id: r.id.into(),
 | 
			
		||||
            title: r.title,
 | 
			
		||||
            content: r.content,
 | 
			
		||||
            timestamp: DateTime::from_timestamp_millis(Ulid::from(r.id).timestamp_ms() as i64)
 | 
			
		||||
                .map(|d| d.to_rfc3339())
 | 
			
		||||
                .unwrap_or("<invalid timestamp>".to_string()),
 | 
			
		||||
        })
 | 
			
		||||
        .collect();
 | 
			
		||||
    Ok(ShowPastesTemplate { pastes })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Template)]
 | 
			
		||||
#[template(path = "admin/paste-new.html")]
 | 
			
		||||
struct CreatePasteTemplate {
 | 
			
		||||
    paste_title: String,
 | 
			
		||||
    paste_content: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Deserialize)]
 | 
			
		||||
| 
						 | 
				
			
			@ -69,21 +111,11 @@ struct CreatePasteFieldsOptional {
 | 
			
		|||
 | 
			
		||||
async fn show_create_paste(
 | 
			
		||||
    Query(CreatePasteFieldsOptional { title, content }): Query<CreatePasteFieldsOptional>,
 | 
			
		||||
) -> Markup {
 | 
			
		||||
    page(
 | 
			
		||||
        "Create Paste",
 | 
			
		||||
        &[BC_INDEX, BC_ADMIN, BC_PASTES],
 | 
			
		||||
        html! {
 | 
			
		||||
            form method="post" action="/admin/pastes" {
 | 
			
		||||
                fieldset {
 | 
			
		||||
                    legend { "Paste" }
 | 
			
		||||
                    input class="w-full" type="text" name="title" placeholder="Paste title" required value=(title.unwrap_or_default());
 | 
			
		||||
                    textarea class="w-full min-h-32" name="content" placeholder="Paste content" required { (content.unwrap_or_default()) }
 | 
			
		||||
                    input type="submit" value="Create";
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
) -> CreatePasteTemplate {
 | 
			
		||||
    CreatePasteTemplate {
 | 
			
		||||
        paste_title: title.unwrap_or_default(),
 | 
			
		||||
        paste_content: content.unwrap_or_default(),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Deserialize)]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,42 +1,60 @@
 | 
			
		|||
// 01HZT59RTH4R6P1TYE6NMAFYEP 018FF454-E351-260D-60EB-CE3568A7F9D6
 | 
			
		||||
 | 
			
		||||
use askama_axum::Template;
 | 
			
		||||
use axum::{
 | 
			
		||||
    extract::{Path, State},
 | 
			
		||||
    routing::get,
 | 
			
		||||
    Router,
 | 
			
		||||
};
 | 
			
		||||
use maud::{html, Markup};
 | 
			
		||||
use sqlx::query;
 | 
			
		||||
use ulid::Ulid;
 | 
			
		||||
use uuid::Uuid;
 | 
			
		||||
 | 
			
		||||
use super::{page, Breadcrumb, BC_INDEX};
 | 
			
		||||
use crate::{app::SharedState, error::AppError};
 | 
			
		||||
 | 
			
		||||
pub(super) fn router() -> Router<SharedState> {
 | 
			
		||||
    Router::new().route("/:id", get(show_paste))
 | 
			
		||||
    Router::new()
 | 
			
		||||
        .route("/:id", get(show_paste))
 | 
			
		||||
        .route("/:id/raw", get(show_paste_raw))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const BC_PASTES_PUBLIC: Breadcrumb = Breadcrumb::new_static("Pastes", "/p");
 | 
			
		||||
#[derive(Template)]
 | 
			
		||||
#[template(path = "paste.html")]
 | 
			
		||||
struct PasteTemplate {
 | 
			
		||||
    id: Ulid,
 | 
			
		||||
    title: String,
 | 
			
		||||
    content: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn show_paste(
 | 
			
		||||
    Path(id): Path<Ulid>,
 | 
			
		||||
    State(SharedState { db, .. }): State<SharedState>,
 | 
			
		||||
) -> Result<Markup, AppError> {
 | 
			
		||||
) -> Result<PasteTemplate, AppError> {
 | 
			
		||||
    match query!(
 | 
			
		||||
        "SELECT title, content FROM paste WHERE id = $1",
 | 
			
		||||
        Uuid::from(id)
 | 
			
		||||
        Uuid::from(id),
 | 
			
		||||
    )
 | 
			
		||||
    .fetch_optional(&db)
 | 
			
		||||
    .await?
 | 
			
		||||
    {
 | 
			
		||||
        Some(r) => Ok(page(
 | 
			
		||||
            &r.title,
 | 
			
		||||
            &[BC_INDEX, BC_PASTES_PUBLIC],
 | 
			
		||||
            html! {
 | 
			
		||||
                pre class="p-4 border border-bd-base rounded-xl" { (r.content) }
 | 
			
		||||
            },
 | 
			
		||||
        )),
 | 
			
		||||
        Some(r) => Ok(PasteTemplate {
 | 
			
		||||
            id,
 | 
			
		||||
            title: r.title,
 | 
			
		||||
            content: r.content,
 | 
			
		||||
        }),
 | 
			
		||||
        None => Err(AppError::PasteNotFound(id)),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn show_paste_raw(
 | 
			
		||||
    Path(id): Path<Ulid>,
 | 
			
		||||
    State(SharedState { db, .. }): State<SharedState>,
 | 
			
		||||
) -> Result<String, AppError> {
 | 
			
		||||
    match query!("SELECT content FROM paste WHERE id = $1", Uuid::from(id))
 | 
			
		||||
        .fetch_optional(&db)
 | 
			
		||||
        .await?
 | 
			
		||||
    {
 | 
			
		||||
        Some(r) => Ok(r.content),
 | 
			
		||||
        None => Err(AppError::PasteNotFound(id)),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										140
									
								
								src/app/resource.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								src/app/resource.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,140 @@
 | 
			
		|||
use axum::{routing::MethodRouter, Router};
 | 
			
		||||
use axum_codec::{
 | 
			
		||||
    handler::Input,
 | 
			
		||||
    routing::{delete, get, post, put},
 | 
			
		||||
    CodecHandler, IntoCodecResponse,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/// A resource which defines a set of conventional CRUD routes.
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
#[must_use]
 | 
			
		||||
pub struct Resource<S = ()> {
 | 
			
		||||
    pub(crate) name: String,
 | 
			
		||||
    pub(crate) router: Router<S>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<S> Resource<S>
 | 
			
		||||
where
 | 
			
		||||
    S: Clone + Send + Sync + 'static,
 | 
			
		||||
{
 | 
			
		||||
    /// Create a `Resource` with the given name.
 | 
			
		||||
    ///
 | 
			
		||||
    /// All routes will be nested at `/{resource_name}`.
 | 
			
		||||
    pub fn named(resource_name: &str) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            name: resource_name.to_owned(),
 | 
			
		||||
            router: Router::new(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Add a handler at `GET /{resource_name}`.
 | 
			
		||||
    pub fn index<H, I, D, T>(self, handler: H) -> Self
 | 
			
		||||
    where
 | 
			
		||||
        H: CodecHandler<T, I, D, S> + Clone + Send + Sync + 'static,
 | 
			
		||||
        I: Input + Send + 'static,
 | 
			
		||||
        D: IntoCodecResponse + Send + Sync + 'static,
 | 
			
		||||
        S: Clone + Send + Sync + 'static,
 | 
			
		||||
        T: 'static,
 | 
			
		||||
    {
 | 
			
		||||
        let path = self.index_create_path();
 | 
			
		||||
        self.route(&path, get(handler).into())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Add a handler at `POST /{resource_name}`.
 | 
			
		||||
    pub fn create<H, I, D, T>(self, handler: H) -> Self
 | 
			
		||||
    where
 | 
			
		||||
        H: CodecHandler<T, I, D, S> + Clone + Send + Sync + 'static,
 | 
			
		||||
        I: Input + Send + 'static,
 | 
			
		||||
        D: IntoCodecResponse + Send + Sync + 'static,
 | 
			
		||||
        S: Clone + Send + Sync + 'static,
 | 
			
		||||
        T: 'static,
 | 
			
		||||
    {
 | 
			
		||||
        let path = self.index_create_path();
 | 
			
		||||
        self.route(&path, post(handler).into())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Add a handler at `GET /{resource_name}/new`.
 | 
			
		||||
    pub fn new<H, I, D, T>(self, handler: H) -> Self
 | 
			
		||||
    where
 | 
			
		||||
        H: CodecHandler<T, I, D, S> + Clone + Send + Sync + 'static,
 | 
			
		||||
        I: Input + Send + 'static,
 | 
			
		||||
        D: IntoCodecResponse + Send + Sync + 'static,
 | 
			
		||||
        S: Clone + Send + Sync + 'static,
 | 
			
		||||
        T: 'static,
 | 
			
		||||
    {
 | 
			
		||||
        let path = format!("/{}/new", self.name);
 | 
			
		||||
        self.route(&path, get(handler).into())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Add a handler at `GET /{resource_name}/:{resource_name}_id`.
 | 
			
		||||
    pub fn show<H, I, D, T>(self, handler: H) -> Self
 | 
			
		||||
    where
 | 
			
		||||
        H: CodecHandler<T, I, D, S> + Clone + Send + Sync + 'static,
 | 
			
		||||
        I: Input + Send + 'static,
 | 
			
		||||
        D: IntoCodecResponse + Send + Sync + 'static,
 | 
			
		||||
        S: Clone + Send + Sync + 'static,
 | 
			
		||||
        T: 'static,
 | 
			
		||||
    {
 | 
			
		||||
        let path = self.show_update_destroy_path();
 | 
			
		||||
        self.route(&path, get(handler).into())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Add a handler at `GET /{resource_name}/:{resource_name}_id/edit`.
 | 
			
		||||
    pub fn edit<H, I, D, T>(self, handler: H) -> Self
 | 
			
		||||
    where
 | 
			
		||||
        H: CodecHandler<T, I, D, S> + Clone + Send + Sync + 'static,
 | 
			
		||||
        I: Input + Send + 'static,
 | 
			
		||||
        D: IntoCodecResponse + Send + Sync + 'static,
 | 
			
		||||
        S: Clone + Send + Sync + 'static,
 | 
			
		||||
        T: 'static,
 | 
			
		||||
    {
 | 
			
		||||
        let path = format!("/{0}/:{0}_id/edit", self.name);
 | 
			
		||||
        self.route(&path, get(handler).into())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Add a handler at `PUT or PATCH /resource_name/:{resource_name}_id`.
 | 
			
		||||
    pub fn update<H, I, D, T>(self, handler: H) -> Self
 | 
			
		||||
    where
 | 
			
		||||
        H: CodecHandler<T, I, D, S> + Clone + Send + Sync + 'static,
 | 
			
		||||
        I: Input + Send + 'static,
 | 
			
		||||
        D: IntoCodecResponse + Send + Sync + 'static,
 | 
			
		||||
        S: Clone + Send + Sync + 'static,
 | 
			
		||||
        T: 'static,
 | 
			
		||||
    {
 | 
			
		||||
        let path = self.show_update_destroy_path();
 | 
			
		||||
        // it's 12 AM and I don't know how to get `MethodFilter` to work
 | 
			
		||||
        self.route(&path, put(handler.clone()).patch(handler).into())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Add a handler at `DELETE /{resource_name}/:{resource_name}_id`.
 | 
			
		||||
    pub fn destroy<H, I, D, T>(self, handler: H) -> Self
 | 
			
		||||
    where
 | 
			
		||||
        H: CodecHandler<T, I, D, S> + Clone + Send + Sync + 'static,
 | 
			
		||||
        I: Input + Send + 'static,
 | 
			
		||||
        D: IntoCodecResponse + Send + Sync + 'static,
 | 
			
		||||
        S: Clone + Send + Sync + 'static,
 | 
			
		||||
        T: 'static,
 | 
			
		||||
    {
 | 
			
		||||
        let path = self.show_update_destroy_path();
 | 
			
		||||
        self.route(&path, delete(handler).into())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn index_create_path(&self) -> String {
 | 
			
		||||
        format!("/{}", self.name)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn show_update_destroy_path(&self) -> String {
 | 
			
		||||
        format!("/{0}/:{0}_id", self.name)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn route(mut self, path: &str, method_router: MethodRouter<S>) -> Self {
 | 
			
		||||
        self.router = self.router.route(path, method_router);
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<S> From<Resource<S>> for Router<S> {
 | 
			
		||||
    fn from(resource: Resource<S>) -> Self {
 | 
			
		||||
        resource.router
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										32
									
								
								src/error.rs
									
										
									
									
									
								
							
							
						
						
									
										32
									
								
								src/error.rs
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,4 +1,5 @@
 | 
			
		|||
use axum::{body::Body, response::IntoResponse};
 | 
			
		||||
use axum_codec::{ContentType, IntoCodecResponse};
 | 
			
		||||
use http::StatusCode;
 | 
			
		||||
use tracing::{error, field};
 | 
			
		||||
use ulid::Ulid;
 | 
			
		||||
| 
						 | 
				
			
			@ -55,3 +56,34 @@ impl IntoResponse for AppError {
 | 
			
		|||
        .into_response()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl IntoCodecResponse for AppError {
 | 
			
		||||
    fn into_codec_response(self, content_type: ContentType) -> axum_core::response::Response<Body> {
 | 
			
		||||
        error!(err = field::display(&self));
 | 
			
		||||
        match self {
 | 
			
		||||
            Self::LinkExists(_) => (StatusCode::BAD_REQUEST, "Link already exists"),
 | 
			
		||||
            Self::LinkNotFoundId(_) | Self::LinkNotFoundSlug(_) => {
 | 
			
		||||
                (StatusCode::NOT_FOUND, "Link not found")
 | 
			
		||||
            }
 | 
			
		||||
            Self::PasteNotFound(_) => (StatusCode::NOT_FOUND, "Paste not found"),
 | 
			
		||||
            Self::ImpossibleAffectedRows(_) => (
 | 
			
		||||
                StatusCode::INTERNAL_SERVER_ERROR,
 | 
			
		||||
                "Database returned an impossible number of affected rows",
 | 
			
		||||
            ),
 | 
			
		||||
            Self::UlidConflict(_) => (StatusCode::INTERNAL_SERVER_ERROR, "ULID conflict real???"),
 | 
			
		||||
            Self::Database(_) => (
 | 
			
		||||
                StatusCode::INTERNAL_SERVER_ERROR,
 | 
			
		||||
                "A database error has occured",
 | 
			
		||||
            ),
 | 
			
		||||
            Self::Io(_) => (
 | 
			
		||||
                StatusCode::INTERNAL_SERVER_ERROR,
 | 
			
		||||
                "An I/O error has occured",
 | 
			
		||||
            ),
 | 
			
		||||
            Self::Other(_) => (
 | 
			
		||||
                StatusCode::INTERNAL_SERVER_ERROR,
 | 
			
		||||
                "An unknown error has occured",
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
        .into_codec_response(content_type)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										1
									
								
								static/.map
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								static/.map
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
{"version":3,"sourceRoot":"","sources":["styles/tailwind/_border-radius.scss","styles/tailwind/_colors.scss","styles/tailwind/_spacing.scss","styles/_theme.scss","styles/style.scss","styles/tailwind/_text.scss"],"names":[],"mappings":"AAAA;ACAA;ACAA;AAAA;AAAA;AAOA;ACDA;EACE;;;AAGF;AACA;AACE;EACA;EACA;EACA;AAEA;EACA;EACA;EACA;AAEA;EACA;EACA;;AAEA;EAfF;AAgBI;IACA;IACA;IACA;AAEA;IACA;IACA;IACA;AAEA;IACA;IACA;;;;AClCJ;EACI;;;AAGJ;EACI;EACA;EACA;EACA;EACA,WFEY;;;AEChB;EAEI;;AAEA;EACI,WFJQ;;AEOZ;EARJ;IASQ;;;AAGJ;EACI;;AAEA;EACI;EACA;EACA,WC7BI;ED8BJ,aCDY;;ADKpB;EACI;EACA;EACA;;;AAKJ;EACI;EACA;EACA;;AAEA;EACI;EACA;EACA;;AAEA;AAAA;EAEI;EACA;EACA;EACA,eJ1DR;EI2DQ;EACA;EACA,WCjED;EDkEC;;AAEA;AAAA;AAAA;EAEI;EACA;;AAOZ;EAEI;EACA;EACA;EACA,eJ9EJ;EI+EI","file":"static"}
 | 
			
		||||
							
								
								
									
										179
									
								
								static/admin.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										179
									
								
								static/admin.css
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,179 @@
 | 
			
		|||
/* https://github.com/tailwindlabs/tailwindcss/blob/ac6d4a6e8e9ae7ca197bf98d933ae2f205be3635/packages/tailwindcss/src/compat/default-theme.ts#L162 */
 | 
			
		||||
/* Tailwind palette */
 | 
			
		||||
/* Yes, it's that simple.
 | 
			
		||||
 * https://tailwindcss.com/docs/customizing-spacing#default-spacing-scale
 | 
			
		||||
 * https://github.com/tailwindlabs/tailwindcss/blob/ac6d4a6e8e9ae7ca197bf98d933ae2f205be3635/packages/tailwindcss/src/compat/default-theme.ts#L909 */
 | 
			
		||||
/* https://github.com/tailwindlabs/tailwindcss/blob/ac6d4a6e8e9ae7ca197bf98d933ae2f205be3635/packages/tailwindcss/src/compat/default-theme.ts#L715 */
 | 
			
		||||
html {
 | 
			
		||||
  color-scheme: light dark;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* These need switchable client-side */
 | 
			
		||||
:root {
 | 
			
		||||
  /* Foreground */
 | 
			
		||||
  --color-fg-base: #334155;
 | 
			
		||||
  --color-fg-deemphasized: #475569;
 | 
			
		||||
  --color-fg-headings: #0f172a;
 | 
			
		||||
  /* Background */
 | 
			
		||||
  --color-bg-base: #f1f5f9;
 | 
			
		||||
  --color-bg-raised-1: #e2e8f0;
 | 
			
		||||
  --color-bg-raised-2: #cbd5e1;
 | 
			
		||||
  --color-bg-raised-3: #94a3b8;
 | 
			
		||||
  /* Borders */
 | 
			
		||||
  --color-bd-base: #cbd5e1;
 | 
			
		||||
  --color-bd-highlighted-1: #64748b;
 | 
			
		||||
  --color-bd-highlighted-2: #475569;
 | 
			
		||||
}
 | 
			
		||||
@media (prefers-color-scheme: dark) {
 | 
			
		||||
  :root {
 | 
			
		||||
    /* Foreground */
 | 
			
		||||
    --color-fg-base: #cbd5e1;
 | 
			
		||||
    --color-fg-deemphasized: #94a3b8;
 | 
			
		||||
    --color-fg-headings: #f1f5f9;
 | 
			
		||||
    /* Background */
 | 
			
		||||
    --color-bg-base: #0f172a;
 | 
			
		||||
    --color-bg-raised-1: #1e293b;
 | 
			
		||||
    --color-bg-raised-2: #334155;
 | 
			
		||||
    /* Borders */
 | 
			
		||||
    --color-bd-base: #334155;
 | 
			
		||||
    --color-bd-highlighted: #64748b;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body.admin {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  max-width: 64rem;
 | 
			
		||||
  padding: 4rem 2rem;
 | 
			
		||||
  margin: 0 auto;
 | 
			
		||||
  display: grid;
 | 
			
		||||
  grid-template-columns: 16rem auto;
 | 
			
		||||
  grid-template-rows: auto auto auto;
 | 
			
		||||
  grid-template-areas: "logo none" "sidebar main" "footer footer";
 | 
			
		||||
  gap: 1rem;
 | 
			
		||||
}
 | 
			
		||||
body.admin > h1 {
 | 
			
		||||
  color: var(--color-fg-headings);
 | 
			
		||||
  font-weight: 700;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  font-size: 2.25rem;
 | 
			
		||||
  grid-area: logo;
 | 
			
		||||
}
 | 
			
		||||
body.admin > aside[role=navigation] {
 | 
			
		||||
  grid-area: sidebar;
 | 
			
		||||
}
 | 
			
		||||
body.admin > aside[role=navigation] ul {
 | 
			
		||||
  list-style: none;
 | 
			
		||||
  margin: unset;
 | 
			
		||||
  padding: unset;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  gap: 0.5rem;
 | 
			
		||||
}
 | 
			
		||||
body.admin > aside[role=navigation] a {
 | 
			
		||||
  display: block;
 | 
			
		||||
  padding: 0.75rem 1rem;
 | 
			
		||||
  color: unset;
 | 
			
		||||
  text-decoration: unset;
 | 
			
		||||
  border: 1px solid var(--color-bd-base);
 | 
			
		||||
  border-radius: 0.75rem;
 | 
			
		||||
  transition-duration: 150ms;
 | 
			
		||||
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
 | 
			
		||||
  transition-property: background-color, border-color;
 | 
			
		||||
  background-color: unset;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
body.admin > aside[role=navigation] a:hover, body.admin > aside[role=navigation] a:focus-visible {
 | 
			
		||||
  border-color: var(--color-bd-highlighted-1);
 | 
			
		||||
  background-color: var(--color-bg-raised-1);
 | 
			
		||||
}
 | 
			
		||||
body.admin > aside[role=navigation] a.active- {
 | 
			
		||||
  border-color: var(--color-bd-highlighted-1);
 | 
			
		||||
  background-color: var(--color-bg-raised-1);
 | 
			
		||||
}
 | 
			
		||||
body.admin > aside[role=navigation] a.active-:hover, body.admin > aside[role=navigation] a.active-:focus-visible {
 | 
			
		||||
  border-color: var(--color-bd-highlighted-2);
 | 
			
		||||
  background-color: var(--color-bg-raised-2);
 | 
			
		||||
}
 | 
			
		||||
body.admin > main {
 | 
			
		||||
  grid-area: main;
 | 
			
		||||
}
 | 
			
		||||
body.admin > footer {
 | 
			
		||||
  grid-area: footer;
 | 
			
		||||
  color: var(--color-fg-deemphasized);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body.admin-pastes main > ul {
 | 
			
		||||
  list-style: none;
 | 
			
		||||
  margin: unset;
 | 
			
		||||
  padding: unset;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  gap: 1.5rem;
 | 
			
		||||
}
 | 
			
		||||
body.admin-pastes main > ul li {
 | 
			
		||||
  display: grid;
 | 
			
		||||
  grid-template-columns: auto auto;
 | 
			
		||||
  grid-template-rows: auto auto;
 | 
			
		||||
  grid-template-areas: "title buttons" "timestamp buttons" "content content";
 | 
			
		||||
  gap: 1rem;
 | 
			
		||||
}
 | 
			
		||||
body.admin-pastes main > ul li .title {
 | 
			
		||||
  grid-area: title;
 | 
			
		||||
  color: var(--color-fg-headings);
 | 
			
		||||
  font-weight: 700;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  font-size: 1.5rem;
 | 
			
		||||
  text-decoration: none;
 | 
			
		||||
}
 | 
			
		||||
body.admin-pastes main > ul li .title:hover, body.admin-pastes main > ul li .title:focus-visible {
 | 
			
		||||
  text-decoration: underline;
 | 
			
		||||
}
 | 
			
		||||
body.admin-pastes main > ul li time {
 | 
			
		||||
  grid-area: timestamp;
 | 
			
		||||
  color: var(--color-fg-deemphasized);
 | 
			
		||||
}
 | 
			
		||||
body.admin-pastes main > ul li .action-buttons {
 | 
			
		||||
  grid-area: buttons;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: end;
 | 
			
		||||
  align-items: end;
 | 
			
		||||
  gap: 0.5rem;
 | 
			
		||||
}
 | 
			
		||||
body.admin-pastes main > ul li .action-buttons button {
 | 
			
		||||
  border: 1px solid var(--color-bd-base);
 | 
			
		||||
  border-radius: 0.75rem;
 | 
			
		||||
  transition-duration: 150ms;
 | 
			
		||||
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
 | 
			
		||||
  transition-property: background-color, border-color;
 | 
			
		||||
  background-color: var(--color-bg-raised-1);
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  display: block;
 | 
			
		||||
  padding: 0.25rem 0.75rem;
 | 
			
		||||
  font-size: 0.875rem;
 | 
			
		||||
  color: unset;
 | 
			
		||||
}
 | 
			
		||||
body.admin-pastes main > ul li .action-buttons button:hover, body.admin-pastes main > ul li .action-buttons button:focus-visible {
 | 
			
		||||
  border-color: var(--color-bd-highlighted-1);
 | 
			
		||||
  background-color: var(--color-bg-raised-2);
 | 
			
		||||
}
 | 
			
		||||
body.admin-pastes main > ul li .action-buttons button.active- {
 | 
			
		||||
  border-color: var(--color-bd-highlighted-1);
 | 
			
		||||
  background-color: var(--color-bg-raised-2);
 | 
			
		||||
}
 | 
			
		||||
body.admin-pastes main > ul li .action-buttons button.active-:hover, body.admin-pastes main > ul li .action-buttons button.active-:focus-visible {
 | 
			
		||||
  border-color: var(--color-bd-highlighted-2);
 | 
			
		||||
  background-color: var(--color-bg-raised-3);
 | 
			
		||||
}
 | 
			
		||||
body.admin-pastes main > ul li .content {
 | 
			
		||||
  grid-area: content;
 | 
			
		||||
  border: 1px solid var(--color-bd-base);
 | 
			
		||||
  border-radius: 0.75rem;
 | 
			
		||||
  transition-duration: 150ms;
 | 
			
		||||
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
 | 
			
		||||
  transition-property: background-color, border-color;
 | 
			
		||||
  background-color: var(--color-bg-raised-1);
 | 
			
		||||
  padding: 1rem;
 | 
			
		||||
  margin: unset;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*# sourceMappingURL=admin.css.map */
 | 
			
		||||
							
								
								
									
										1
									
								
								static/admin.css.map
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								static/admin.css.map
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
{"version":3,"sourceRoot":"","sources":["../styles/tailwind/_border-radius.scss","../styles/tailwind/_colors.scss","../styles/tailwind/_spacing.scss","../styles/_theme.scss","../styles/admin.scss","../styles/tailwind/_text.scss","../styles/tailwind/_transition.scss"],"names":[],"mappings":"AAAA;ACAA;ACAA;AAAA;AAAA;AAOA;ACDA;EACI;;;AAGJ;AACA;AACI;EACA;EACA;EACA;AAEA;EACA;EACA;EACA;EACA;AAEA;EACA;EACA;EACA;;AAEA;EAjBJ;AAkBQ;IACA;IACA;IACA;AAEA;IACA;IACA;IACA;AAEA;IACA;IACA;;;;ACtCR;EACI;EACA,WFYY;EEXZ;EACA;EACA;EACA;EACA;EACA,qBACI;EAGJ;;AAEA;ED6BA;EACA,aEbe;EFeX;EAMJ,WEhDY;EDYR;;AAGJ;EACI;;AAEA;EACI;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;ED+BR;EACA,eHhEI;EGiEJ,qBGjEe;EHkEf,4BGxEgB;EHyEhB;EAKI;EAIA;;AAEA;EAEI;EAII;;AAIR;EACI;EAII;;AAGJ;EAEI;EAII;;ACjEhB;EACI;;AAGJ;EACI;EACA;;;AAKJ;EACI;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA,qBACI;EAGJ;;AAEA;EACI;ED3BZ;EACA,aEbe;EFeX;EAgBJ,WE5DY;EDsEA;;AAEA;EAEI;;AAIR;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;;AAEA;ED1BZ;EACA,eHhEI;EGiEJ,qBGjEe;EHkEf,4BGxEgB;EHyEhB;EAGI;EAMA;EAmCJ;EACA;EACA,WEtHW;EFuHX;;AApCI;EAEI;EAEI;;AAMR;EACI;EAEI;;AAKJ;EAEI;EAEI;;ACNR;EACI;EDhCZ;EACA,eHhEI;EGiEJ,qBGjEe;EHkEf,4BGxEgB;EHyEhB;EAGI;EC2BQ;EACA","file":"admin.css"}
 | 
			
		||||
| 
						 | 
				
			
			@ -6,3 +6,9 @@ document
 | 
			
		|||
                e.getAttribute("datetime")
 | 
			
		||||
            ).toLocaleString())
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
function copyPasteContent() {
 | 
			
		||||
    navigator.clipboard
 | 
			
		||||
        .writeText(document.getElementById("paste-content").textContent)
 | 
			
		||||
        .catch((e) => console.error(e));
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										875
									
								
								static/style.css
									
										
									
									
									
								
							
							
						
						
									
										875
									
								
								static/style.css
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,795 +1,116 @@
 | 
			
		|||
/*
 | 
			
		||||
! tailwindcss v3.4.3 | MIT License | https://tailwindcss.com
 | 
			
		||||
*/
 | 
			
		||||
/* https://github.com/tailwindlabs/tailwindcss/blob/ac6d4a6e8e9ae7ca197bf98d933ae2f205be3635/packages/tailwindcss/src/compat/default-theme.ts#L162 */
 | 
			
		||||
/* Tailwind palette */
 | 
			
		||||
/* Yes, it's that simple.
 | 
			
		||||
 * https://tailwindcss.com/docs/customizing-spacing#default-spacing-scale
 | 
			
		||||
 * https://github.com/tailwindlabs/tailwindcss/blob/ac6d4a6e8e9ae7ca197bf98d933ae2f205be3635/packages/tailwindcss/src/compat/default-theme.ts#L909 */
 | 
			
		||||
/* https://github.com/tailwindlabs/tailwindcss/blob/ac6d4a6e8e9ae7ca197bf98d933ae2f205be3635/packages/tailwindcss/src/compat/default-theme.ts#L715 */
 | 
			
		||||
html {
 | 
			
		||||
  color-scheme: light dark;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
 | 
			
		||||
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
 | 
			
		||||
*/
 | 
			
		||||
/* These need switchable client-side */
 | 
			
		||||
:root {
 | 
			
		||||
  /* Foreground */
 | 
			
		||||
  --color-fg-base: #334155;
 | 
			
		||||
  --color-fg-deemphasized: #475569;
 | 
			
		||||
  --color-fg-headings: #0f172a;
 | 
			
		||||
  /* Background */
 | 
			
		||||
  --color-bg-base: #f1f5f9;
 | 
			
		||||
  --color-bg-raised-1: #e2e8f0;
 | 
			
		||||
  --color-bg-raised-2: #cbd5e1;
 | 
			
		||||
  --color-bg-raised-3: #94a3b8;
 | 
			
		||||
  /* Borders */
 | 
			
		||||
  --color-bd-base: #cbd5e1;
 | 
			
		||||
  --color-bd-highlighted-1: #64748b;
 | 
			
		||||
  --color-bd-highlighted-2: #475569;
 | 
			
		||||
}
 | 
			
		||||
@media (prefers-color-scheme: dark) {
 | 
			
		||||
  :root {
 | 
			
		||||
    /* Foreground */
 | 
			
		||||
    --color-fg-base: #cbd5e1;
 | 
			
		||||
    --color-fg-deemphasized: #94a3b8;
 | 
			
		||||
    --color-fg-headings: #f1f5f9;
 | 
			
		||||
    /* Background */
 | 
			
		||||
    --color-bg-base: #0f172a;
 | 
			
		||||
    --color-bg-raised-1: #1e293b;
 | 
			
		||||
    --color-bg-raised-2: #334155;
 | 
			
		||||
    /* Borders */
 | 
			
		||||
    --color-bd-base: #334155;
 | 
			
		||||
    --color-bd-highlighted: #64748b;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
*,
 | 
			
		||||
::before,
 | 
			
		||||
::after {
 | 
			
		||||
* {
 | 
			
		||||
  box-sizing: border-box;
 | 
			
		||||
  /* 1 */
 | 
			
		||||
  border-width: 0;
 | 
			
		||||
  /* 2 */
 | 
			
		||||
  border-style: solid;
 | 
			
		||||
  /* 2 */
 | 
			
		||||
  border-color: #e5e7eb;
 | 
			
		||||
  /* 2 */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
::before,
 | 
			
		||||
::after {
 | 
			
		||||
  --tw-content: '';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
1. Use a consistent sensible line-height in all browsers.
 | 
			
		||||
2. Prevent adjustments of font size after orientation changes in iOS.
 | 
			
		||||
3. Use a more readable tab size.
 | 
			
		||||
4. Use the user's configured `sans` font-family by default.
 | 
			
		||||
5. Use the user's configured `sans` font-feature-settings by default.
 | 
			
		||||
6. Use the user's configured `sans` font-variation-settings by default.
 | 
			
		||||
7. Disable tap highlights on iOS
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
html,
 | 
			
		||||
:host {
 | 
			
		||||
  line-height: 1.5;
 | 
			
		||||
  /* 1 */
 | 
			
		||||
  -webkit-text-size-adjust: 100%;
 | 
			
		||||
  /* 2 */
 | 
			
		||||
  -moz-tab-size: 4;
 | 
			
		||||
  /* 3 */
 | 
			
		||||
  -o-tab-size: 4;
 | 
			
		||||
     tab-size: 4;
 | 
			
		||||
  /* 3 */
 | 
			
		||||
  font-family: sans-serif;
 | 
			
		||||
  /* 4 */
 | 
			
		||||
  font-feature-settings: normal;
 | 
			
		||||
  /* 5 */
 | 
			
		||||
  font-variation-settings: normal;
 | 
			
		||||
  /* 6 */
 | 
			
		||||
  -webkit-tap-highlight-color: transparent;
 | 
			
		||||
  /* 7 */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
1. Remove the margin in all browsers.
 | 
			
		||||
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
body {
 | 
			
		||||
  font-family: Inter, "Inter Variable", sans-serif;
 | 
			
		||||
  color: var(--color-fg-base);
 | 
			
		||||
  background-color: var(--color-bg-base);
 | 
			
		||||
  margin: 0 auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body:not(.admin) {
 | 
			
		||||
  max-width: 56rem;
 | 
			
		||||
  padding: 4rem 2rem;
 | 
			
		||||
}
 | 
			
		||||
body:not(.admin).wide {
 | 
			
		||||
  max-width: 72rem;
 | 
			
		||||
}
 | 
			
		||||
@media not all and (min-width: var(--max-width-lg)) {
 | 
			
		||||
  body:not(.admin) {
 | 
			
		||||
    padding: 2rem;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
body:not(.admin) header {
 | 
			
		||||
  margin-bottom: 2rem;
 | 
			
		||||
}
 | 
			
		||||
body:not(.admin) header h1 {
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  /* 1 */
 | 
			
		||||
  line-height: inherit;
 | 
			
		||||
  /* 2 */
 | 
			
		||||
  color: var(--color-fg-headings);
 | 
			
		||||
  font-size: 1.875rem;
 | 
			
		||||
  font-weight: 800;
 | 
			
		||||
}
 | 
			
		||||
body:not(.admin) footer {
 | 
			
		||||
  color: var(--color-fg-deemphasized);
 | 
			
		||||
  margin-top: 2rem;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
1. Add the correct height in Firefox.
 | 
			
		||||
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
 | 
			
		||||
3. Ensure horizontal rules are visible by default.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
hr {
 | 
			
		||||
  height: 0;
 | 
			
		||||
  /* 1 */
 | 
			
		||||
  color: inherit;
 | 
			
		||||
  /* 2 */
 | 
			
		||||
  border-top-width: 1px;
 | 
			
		||||
  /* 3 */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
Add the correct text decoration in Chrome, Edge, and Safari.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
abbr:where([title]) {
 | 
			
		||||
  -webkit-text-decoration: underline dotted;
 | 
			
		||||
          text-decoration: underline dotted;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
Remove the default font size and weight for headings.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
h1,
 | 
			
		||||
h2,
 | 
			
		||||
h3,
 | 
			
		||||
h4,
 | 
			
		||||
h5,
 | 
			
		||||
h6 {
 | 
			
		||||
  font-size: inherit;
 | 
			
		||||
  font-weight: inherit;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
Reset links to optimize for opt-in styling instead of opt-out.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
a {
 | 
			
		||||
  color: inherit;
 | 
			
		||||
  text-decoration: inherit;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
Add the correct font weight in Edge and Safari.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
b,
 | 
			
		||||
strong {
 | 
			
		||||
  font-weight: bolder;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
1. Use the user's configured `mono` font-family by default.
 | 
			
		||||
2. Use the user's configured `mono` font-feature-settings by default.
 | 
			
		||||
3. Use the user's configured `mono` font-variation-settings by default.
 | 
			
		||||
4. Correct the odd `em` font sizing in all browsers.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
code,
 | 
			
		||||
kbd,
 | 
			
		||||
samp,
 | 
			
		||||
pre {
 | 
			
		||||
  font-family: monospace;
 | 
			
		||||
  /* 1 */
 | 
			
		||||
  font-feature-settings: normal;
 | 
			
		||||
  /* 2 */
 | 
			
		||||
  font-variation-settings: normal;
 | 
			
		||||
  /* 3 */
 | 
			
		||||
  font-size: 1em;
 | 
			
		||||
  /* 4 */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
Add the correct font size in all browsers.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
small {
 | 
			
		||||
  font-size: 80%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
sub,
 | 
			
		||||
sup {
 | 
			
		||||
  font-size: 75%;
 | 
			
		||||
  line-height: 0;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  vertical-align: baseline;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
sub {
 | 
			
		||||
  bottom: -0.25em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
sup {
 | 
			
		||||
  top: -0.5em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
 | 
			
		||||
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
 | 
			
		||||
3. Remove gaps between table borders by default.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
table {
 | 
			
		||||
  text-indent: 0;
 | 
			
		||||
  /* 1 */
 | 
			
		||||
  border-color: inherit;
 | 
			
		||||
  /* 2 */
 | 
			
		||||
  border-collapse: collapse;
 | 
			
		||||
  /* 3 */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
1. Change the font styles in all browsers.
 | 
			
		||||
2. Remove the margin in Firefox and Safari.
 | 
			
		||||
3. Remove default padding in all browsers.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
button,
 | 
			
		||||
input,
 | 
			
		||||
optgroup,
 | 
			
		||||
select,
 | 
			
		||||
textarea {
 | 
			
		||||
  font-family: inherit;
 | 
			
		||||
  /* 1 */
 | 
			
		||||
  font-feature-settings: inherit;
 | 
			
		||||
  /* 1 */
 | 
			
		||||
  font-variation-settings: inherit;
 | 
			
		||||
  /* 1 */
 | 
			
		||||
  font-size: 100%;
 | 
			
		||||
  /* 1 */
 | 
			
		||||
  font-weight: inherit;
 | 
			
		||||
  /* 1 */
 | 
			
		||||
  line-height: inherit;
 | 
			
		||||
  /* 1 */
 | 
			
		||||
  letter-spacing: inherit;
 | 
			
		||||
  /* 1 */
 | 
			
		||||
  color: inherit;
 | 
			
		||||
  /* 1 */
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  /* 2 */
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  /* 3 */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
Remove the inheritance of text transform in Edge and Firefox.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
button,
 | 
			
		||||
select {
 | 
			
		||||
  text-transform: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
1. Correct the inability to style clickable types in iOS and Safari.
 | 
			
		||||
2. Remove default button styles.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
button,
 | 
			
		||||
input:where([type='button']),
 | 
			
		||||
input:where([type='reset']),
 | 
			
		||||
input:where([type='submit']) {
 | 
			
		||||
  -webkit-appearance: button;
 | 
			
		||||
  /* 1 */
 | 
			
		||||
  background-color: transparent;
 | 
			
		||||
  /* 2 */
 | 
			
		||||
  background-image: none;
 | 
			
		||||
  /* 2 */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
Use the modern Firefox focus style for all focusable elements.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
:-moz-focusring {
 | 
			
		||||
  outline: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
:-moz-ui-invalid {
 | 
			
		||||
  box-shadow: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
Add the correct vertical alignment in Chrome and Firefox.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
progress {
 | 
			
		||||
  vertical-align: baseline;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
Correct the cursor style of increment and decrement buttons in Safari.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
::-webkit-inner-spin-button,
 | 
			
		||||
::-webkit-outer-spin-button {
 | 
			
		||||
  height: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
1. Correct the odd appearance in Chrome and Safari.
 | 
			
		||||
2. Correct the outline style in Safari.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
[type='search'] {
 | 
			
		||||
  -webkit-appearance: textfield;
 | 
			
		||||
  /* 1 */
 | 
			
		||||
  outline-offset: -2px;
 | 
			
		||||
  /* 2 */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
Remove the inner padding in Chrome and Safari on macOS.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
::-webkit-search-decoration {
 | 
			
		||||
  -webkit-appearance: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
1. Correct the inability to style clickable types in iOS and Safari.
 | 
			
		||||
2. Change font properties to `inherit` in Safari.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
::-webkit-file-upload-button {
 | 
			
		||||
  -webkit-appearance: button;
 | 
			
		||||
  /* 1 */
 | 
			
		||||
  font: inherit;
 | 
			
		||||
  /* 2 */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
Add the correct display in Chrome and Safari.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
summary {
 | 
			
		||||
  display: list-item;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
Removes the default spacing and border for appropriate elements.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
blockquote,
 | 
			
		||||
dl,
 | 
			
		||||
dd,
 | 
			
		||||
h1,
 | 
			
		||||
h2,
 | 
			
		||||
h3,
 | 
			
		||||
h4,
 | 
			
		||||
h5,
 | 
			
		||||
h6,
 | 
			
		||||
hr,
 | 
			
		||||
figure,
 | 
			
		||||
p,
 | 
			
		||||
pre {
 | 
			
		||||
  margin: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fieldset {
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
legend {
 | 
			
		||||
  padding: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ol,
 | 
			
		||||
ul,
 | 
			
		||||
menu {
 | 
			
		||||
  list-style: none;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
Reset default styling for dialogs.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
dialog {
 | 
			
		||||
  padding: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
Prevent resizing textareas horizontally by default.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
textarea {
 | 
			
		||||
  resize: vertical;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
 | 
			
		||||
2. Set the default placeholder color to the user's configured gray 400 color.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
input::-moz-placeholder, textarea::-moz-placeholder {
 | 
			
		||||
  opacity: 1;
 | 
			
		||||
  /* 1 */
 | 
			
		||||
  color: #9ca3af;
 | 
			
		||||
  /* 2 */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input::placeholder,
 | 
			
		||||
textarea::placeholder {
 | 
			
		||||
  opacity: 1;
 | 
			
		||||
  /* 1 */
 | 
			
		||||
  color: #9ca3af;
 | 
			
		||||
  /* 2 */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
Set the default cursor for buttons.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
button,
 | 
			
		||||
[role="button"] {
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
Make sure disabled buttons don't get the pointer cursor.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
:disabled {
 | 
			
		||||
  cursor: default;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
 | 
			
		||||
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
 | 
			
		||||
   This can trigger a poorly considered lint error in some tools but is included by design.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
img,
 | 
			
		||||
svg,
 | 
			
		||||
video,
 | 
			
		||||
canvas,
 | 
			
		||||
audio,
 | 
			
		||||
iframe,
 | 
			
		||||
embed,
 | 
			
		||||
object {
 | 
			
		||||
  display: block;
 | 
			
		||||
  /* 1 */
 | 
			
		||||
  vertical-align: middle;
 | 
			
		||||
  /* 2 */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
img,
 | 
			
		||||
video {
 | 
			
		||||
  max-width: 100%;
 | 
			
		||||
  height: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Make elements with the HTML hidden attribute stay hidden by default */
 | 
			
		||||
 | 
			
		||||
[hidden] {
 | 
			
		||||
  display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
*, ::before, ::after {
 | 
			
		||||
  --tw-border-spacing-x: 0;
 | 
			
		||||
  --tw-border-spacing-y: 0;
 | 
			
		||||
  --tw-translate-x: 0;
 | 
			
		||||
  --tw-translate-y: 0;
 | 
			
		||||
  --tw-rotate: 0;
 | 
			
		||||
  --tw-skew-x: 0;
 | 
			
		||||
  --tw-skew-y: 0;
 | 
			
		||||
  --tw-scale-x: 1;
 | 
			
		||||
  --tw-scale-y: 1;
 | 
			
		||||
  --tw-pan-x:  ;
 | 
			
		||||
  --tw-pan-y:  ;
 | 
			
		||||
  --tw-pinch-zoom:  ;
 | 
			
		||||
  --tw-scroll-snap-strictness: proximity;
 | 
			
		||||
  --tw-gradient-from-position:  ;
 | 
			
		||||
  --tw-gradient-via-position:  ;
 | 
			
		||||
  --tw-gradient-to-position:  ;
 | 
			
		||||
  --tw-ordinal:  ;
 | 
			
		||||
  --tw-slashed-zero:  ;
 | 
			
		||||
  --tw-numeric-figure:  ;
 | 
			
		||||
  --tw-numeric-spacing:  ;
 | 
			
		||||
  --tw-numeric-fraction:  ;
 | 
			
		||||
  --tw-ring-inset:  ;
 | 
			
		||||
  --tw-ring-offset-width: 0px;
 | 
			
		||||
  --tw-ring-offset-color: #fff;
 | 
			
		||||
  --tw-ring-color: rgb(59 130 246 / 0.5);
 | 
			
		||||
  --tw-ring-offset-shadow: 0 0 #0000;
 | 
			
		||||
  --tw-ring-shadow: 0 0 #0000;
 | 
			
		||||
  --tw-shadow: 0 0 #0000;
 | 
			
		||||
  --tw-shadow-colored: 0 0 #0000;
 | 
			
		||||
  --tw-blur:  ;
 | 
			
		||||
  --tw-brightness:  ;
 | 
			
		||||
  --tw-contrast:  ;
 | 
			
		||||
  --tw-grayscale:  ;
 | 
			
		||||
  --tw-hue-rotate:  ;
 | 
			
		||||
  --tw-invert:  ;
 | 
			
		||||
  --tw-saturate:  ;
 | 
			
		||||
  --tw-sepia:  ;
 | 
			
		||||
  --tw-drop-shadow:  ;
 | 
			
		||||
  --tw-backdrop-blur:  ;
 | 
			
		||||
  --tw-backdrop-brightness:  ;
 | 
			
		||||
  --tw-backdrop-contrast:  ;
 | 
			
		||||
  --tw-backdrop-grayscale:  ;
 | 
			
		||||
  --tw-backdrop-hue-rotate:  ;
 | 
			
		||||
  --tw-backdrop-invert:  ;
 | 
			
		||||
  --tw-backdrop-opacity:  ;
 | 
			
		||||
  --tw-backdrop-saturate:  ;
 | 
			
		||||
  --tw-backdrop-sepia:  ;
 | 
			
		||||
  --tw-contain-size:  ;
 | 
			
		||||
  --tw-contain-layout:  ;
 | 
			
		||||
  --tw-contain-paint:  ;
 | 
			
		||||
  --tw-contain-style:  ;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
::backdrop {
 | 
			
		||||
  --tw-border-spacing-x: 0;
 | 
			
		||||
  --tw-border-spacing-y: 0;
 | 
			
		||||
  --tw-translate-x: 0;
 | 
			
		||||
  --tw-translate-y: 0;
 | 
			
		||||
  --tw-rotate: 0;
 | 
			
		||||
  --tw-skew-x: 0;
 | 
			
		||||
  --tw-skew-y: 0;
 | 
			
		||||
  --tw-scale-x: 1;
 | 
			
		||||
  --tw-scale-y: 1;
 | 
			
		||||
  --tw-pan-x:  ;
 | 
			
		||||
  --tw-pan-y:  ;
 | 
			
		||||
  --tw-pinch-zoom:  ;
 | 
			
		||||
  --tw-scroll-snap-strictness: proximity;
 | 
			
		||||
  --tw-gradient-from-position:  ;
 | 
			
		||||
  --tw-gradient-via-position:  ;
 | 
			
		||||
  --tw-gradient-to-position:  ;
 | 
			
		||||
  --tw-ordinal:  ;
 | 
			
		||||
  --tw-slashed-zero:  ;
 | 
			
		||||
  --tw-numeric-figure:  ;
 | 
			
		||||
  --tw-numeric-spacing:  ;
 | 
			
		||||
  --tw-numeric-fraction:  ;
 | 
			
		||||
  --tw-ring-inset:  ;
 | 
			
		||||
  --tw-ring-offset-width: 0px;
 | 
			
		||||
  --tw-ring-offset-color: #fff;
 | 
			
		||||
  --tw-ring-color: rgb(59 130 246 / 0.5);
 | 
			
		||||
  --tw-ring-offset-shadow: 0 0 #0000;
 | 
			
		||||
  --tw-ring-shadow: 0 0 #0000;
 | 
			
		||||
  --tw-shadow: 0 0 #0000;
 | 
			
		||||
  --tw-shadow-colored: 0 0 #0000;
 | 
			
		||||
  --tw-blur:  ;
 | 
			
		||||
  --tw-brightness:  ;
 | 
			
		||||
  --tw-contrast:  ;
 | 
			
		||||
  --tw-grayscale:  ;
 | 
			
		||||
  --tw-hue-rotate:  ;
 | 
			
		||||
  --tw-invert:  ;
 | 
			
		||||
  --tw-saturate:  ;
 | 
			
		||||
  --tw-sepia:  ;
 | 
			
		||||
  --tw-drop-shadow:  ;
 | 
			
		||||
  --tw-backdrop-blur:  ;
 | 
			
		||||
  --tw-backdrop-brightness:  ;
 | 
			
		||||
  --tw-backdrop-contrast:  ;
 | 
			
		||||
  --tw-backdrop-grayscale:  ;
 | 
			
		||||
  --tw-backdrop-hue-rotate:  ;
 | 
			
		||||
  --tw-backdrop-invert:  ;
 | 
			
		||||
  --tw-backdrop-opacity:  ;
 | 
			
		||||
  --tw-backdrop-saturate:  ;
 | 
			
		||||
  --tw-backdrop-sepia:  ;
 | 
			
		||||
  --tw-contain-size:  ;
 | 
			
		||||
  --tw-contain-layout:  ;
 | 
			
		||||
  --tw-contain-paint:  ;
 | 
			
		||||
  --tw-contain-style:  ;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.static {
 | 
			
		||||
  position: static;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.flex {
 | 
			
		||||
body.paste header {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  align-items: end;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.table {
 | 
			
		||||
  display: table;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.min-h-32 {
 | 
			
		||||
  min-height: 8rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.w-full {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.gap-2 {
 | 
			
		||||
body.paste header .action-buttons {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: end;
 | 
			
		||||
  gap: 0.5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.rounded-xl {
 | 
			
		||||
  border-radius: 0.75rem;
 | 
			
		||||
body.paste header .action-buttons button,
 | 
			
		||||
body.paste header .action-buttons [role=button] {
 | 
			
		||||
  background-color: var(--color-bg-raised-1);
 | 
			
		||||
  padding: 0.25rem 0.75rem;
 | 
			
		||||
  border: 1px solid var(--color-bd-base);
 | 
			
		||||
  border-radius: 0.5rem;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  color: inherit;
 | 
			
		||||
  font-size: 0.875rem;
 | 
			
		||||
  text-decoration: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.border {
 | 
			
		||||
  border-width: 1px;
 | 
			
		||||
body.paste header .action-buttons button:hover, body.paste header .action-buttons button:focus-visible,
 | 
			
		||||
body.paste header .action-buttons [role=button]:hover,
 | 
			
		||||
body.paste header .action-buttons [role=button]:focus-visible {
 | 
			
		||||
  background-color: var(--color-bg-raised-2);
 | 
			
		||||
  border-color: var(--color-bd-highlighted);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.border-bd-base {
 | 
			
		||||
  --tw-border-opacity: 1;
 | 
			
		||||
  border-color: rgb(51 65 85 / var(--tw-border-opacity));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.p-4 {
 | 
			
		||||
body.paste main pre {
 | 
			
		||||
  background-color: var(--color-bg-raised-1);
 | 
			
		||||
  padding: 1rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body {
 | 
			
		||||
  margin-left: auto;
 | 
			
		||||
  margin-right: auto;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  max-width: 56rem;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  gap: 2rem;
 | 
			
		||||
  --tw-bg-opacity: 1;
 | 
			
		||||
  background-color: rgb(15 23 42 / var(--tw-bg-opacity));
 | 
			
		||||
  padding-left: 1rem;
 | 
			
		||||
  padding-right: 1rem;
 | 
			
		||||
  padding-top: 4rem;
 | 
			
		||||
  padding-bottom: 4rem;
 | 
			
		||||
  --tw-text-opacity: 1;
 | 
			
		||||
  color: rgb(203 213 225 / var(--tw-text-opacity));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media not all and (min-width: 1024px) {
 | 
			
		||||
  body {
 | 
			
		||||
    padding-top: 2rem;
 | 
			
		||||
    padding-bottom: 2rem;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
header {
 | 
			
		||||
  div {
 | 
			
		||||
    margin-bottom: 0.25rem;
 | 
			
		||||
  }
 | 
			
		||||
  div {
 | 
			
		||||
    display: flex;
 | 
			
		||||
  }
 | 
			
		||||
  div {
 | 
			
		||||
    gap: 0.5rem;
 | 
			
		||||
  }
 | 
			
		||||
  div {
 | 
			
		||||
    font-size: 1.125rem;
 | 
			
		||||
    line-height: 1.75rem;
 | 
			
		||||
  }
 | 
			
		||||
  div {
 | 
			
		||||
    --tw-text-opacity: 1;
 | 
			
		||||
    color: rgb(148 163 184 / var(--tw-text-opacity));
 | 
			
		||||
  }
 | 
			
		||||
  h1 {
 | 
			
		||||
    font-size: 3rem;
 | 
			
		||||
    line-height: 1;
 | 
			
		||||
  }
 | 
			
		||||
  h1 {
 | 
			
		||||
    font-weight: 800;
 | 
			
		||||
  }
 | 
			
		||||
  h1 {
 | 
			
		||||
    --tw-text-opacity: 1;
 | 
			
		||||
    color: rgb(255 255 255 / var(--tw-text-opacity));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
footer {
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  --tw-text-opacity: 1;
 | 
			
		||||
  color: rgb(148 163 184 / var(--tw-text-opacity));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
a.tl {
 | 
			
		||||
  text-decoration-line: underline;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
a.tl:hover {
 | 
			
		||||
  --tw-text-opacity: 1;
 | 
			
		||||
  color: rgb(255 255 255 / var(--tw-text-opacity));
 | 
			
		||||
  text-decoration-line: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
a.tl:focus-visible {
 | 
			
		||||
  --tw-text-opacity: 1;
 | 
			
		||||
  color: rgb(255 255 255 / var(--tw-text-opacity));
 | 
			
		||||
  text-decoration-line: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
table {
 | 
			
		||||
  table-layout: auto;
 | 
			
		||||
  border-collapse: collapse;
 | 
			
		||||
  border-width: 1px;
 | 
			
		||||
  --tw-border-opacity: 1;
 | 
			
		||||
  border-color: rgb(51 65 85 / var(--tw-border-opacity));
 | 
			
		||||
  thead tr th {
 | 
			
		||||
    border-width: 1px;
 | 
			
		||||
  }
 | 
			
		||||
  thead tr th {
 | 
			
		||||
    --tw-border-opacity: 1;
 | 
			
		||||
    border-color: rgb(51 65 85 / var(--tw-border-opacity));
 | 
			
		||||
  }
 | 
			
		||||
  thead tr th {
 | 
			
		||||
    padding-left: 0.5rem;
 | 
			
		||||
    padding-right: 0.5rem;
 | 
			
		||||
  }
 | 
			
		||||
  thead tr th {
 | 
			
		||||
    padding-top: 0.25rem;
 | 
			
		||||
    padding-bottom: 0.25rem;
 | 
			
		||||
  }
 | 
			
		||||
  thead tr th {
 | 
			
		||||
    text-align: left;
 | 
			
		||||
  }
 | 
			
		||||
  tbody tr td {
 | 
			
		||||
    border-width: 1px;
 | 
			
		||||
  }
 | 
			
		||||
  tbody tr td {
 | 
			
		||||
    --tw-border-opacity: 1;
 | 
			
		||||
    border-color: rgb(51 65 85 / var(--tw-border-opacity));
 | 
			
		||||
  }
 | 
			
		||||
  tbody tr td {
 | 
			
		||||
    padding-left: 0.5rem;
 | 
			
		||||
    padding-right: 0.5rem;
 | 
			
		||||
  }
 | 
			
		||||
  tbody tr td {
 | 
			
		||||
    padding-top: 0.25rem;
 | 
			
		||||
    padding-bottom: 0.25rem;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
form fieldset {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  gap: 1rem;
 | 
			
		||||
  border: 1px solid var(--color-bd-base);
 | 
			
		||||
  border-radius: 0.75rem;
 | 
			
		||||
  border-width: 1px;
 | 
			
		||||
  --tw-border-opacity: 1;
 | 
			
		||||
  border-color: rgb(51 65 85 / var(--tw-border-opacity));
 | 
			
		||||
  padding: 1rem;
 | 
			
		||||
  padding-top: 0.5rem;
 | 
			
		||||
  input[type="text"],
 | 
			
		||||
    textarea {
 | 
			
		||||
    border-radius: 0.5rem;
 | 
			
		||||
  }
 | 
			
		||||
  input[type="text"],
 | 
			
		||||
    textarea {
 | 
			
		||||
    border-width: 1px;
 | 
			
		||||
  }
 | 
			
		||||
  input[type="text"],
 | 
			
		||||
    textarea {
 | 
			
		||||
    --tw-border-opacity: 1;
 | 
			
		||||
    border-color: rgb(51 65 85 / var(--tw-border-opacity));
 | 
			
		||||
  }
 | 
			
		||||
  input[type="text"],
 | 
			
		||||
    textarea {
 | 
			
		||||
    background-color: transparent;
 | 
			
		||||
  }
 | 
			
		||||
  input[type="text"],
 | 
			
		||||
    textarea {
 | 
			
		||||
    padding-left: 0.75rem;
 | 
			
		||||
    padding-right: 0.75rem;
 | 
			
		||||
  }
 | 
			
		||||
  input[type="text"],
 | 
			
		||||
    textarea {
 | 
			
		||||
    padding-top: 0.5rem;
 | 
			
		||||
    padding-bottom: 0.5rem;
 | 
			
		||||
  }
 | 
			
		||||
  input[type="text"]:hover,
 | 
			
		||||
    textarea:hover {
 | 
			
		||||
    --tw-border-opacity: 1;
 | 
			
		||||
    border-color: rgb(100 116 139 / var(--tw-border-opacity));
 | 
			
		||||
  }
 | 
			
		||||
  input[type="text"]:focus-visible,
 | 
			
		||||
    textarea:focus-visible {
 | 
			
		||||
    --tw-border-opacity: 1;
 | 
			
		||||
    border-color: rgb(100 116 139 / var(--tw-border-opacity));
 | 
			
		||||
  }
 | 
			
		||||
  input[type="submit"] {
 | 
			
		||||
    margin-left: auto;
 | 
			
		||||
  }
 | 
			
		||||
  input[type="submit"] {
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
  }
 | 
			
		||||
  input[type="submit"] {
 | 
			
		||||
    border-radius: 0.5rem;
 | 
			
		||||
  }
 | 
			
		||||
  input[type="submit"] {
 | 
			
		||||
    border-width: 1px;
 | 
			
		||||
  }
 | 
			
		||||
  input[type="submit"] {
 | 
			
		||||
    --tw-border-opacity: 1;
 | 
			
		||||
    border-color: rgb(51 65 85 / var(--tw-border-opacity));
 | 
			
		||||
  }
 | 
			
		||||
  input[type="submit"] {
 | 
			
		||||
    padding-left: 0.75rem;
 | 
			
		||||
    padding-right: 0.75rem;
 | 
			
		||||
  }
 | 
			
		||||
  input[type="submit"] {
 | 
			
		||||
    padding-top: 0.5rem;
 | 
			
		||||
    padding-bottom: 0.5rem;
 | 
			
		||||
  }
 | 
			
		||||
  input[type="submit"]:hover {
 | 
			
		||||
    --tw-border-opacity: 1;
 | 
			
		||||
    border-color: rgb(100 116 139 / var(--tw-border-opacity));
 | 
			
		||||
  }
 | 
			
		||||
  input[type="submit"]:focus-visible {
 | 
			
		||||
    --tw-border-opacity: 1;
 | 
			
		||||
    border-color: rgb(100 116 139 / var(--tw-border-opacity));
 | 
			
		||||
  }
 | 
			
		||||
  margin: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*# sourceMappingURL=style.css.map */
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										1
									
								
								static/style.css.map
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								static/style.css.map
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
{"version":3,"sourceRoot":"","sources":["../styles/tailwind/_border-radius.scss","../styles/tailwind/_colors.scss","../styles/tailwind/_spacing.scss","../styles/_theme.scss","../styles/style.scss","../styles/tailwind/_text.scss"],"names":[],"mappings":"AAAA;ACAA;ACAA;AAAA;AAAA;AAOA;ACDA;EACI;;;AAGJ;AACA;AACI;EACA;EACA;EACA;AAEA;EACA;EACA;EACA;EACA;AAEA;EACA;EACA;EACA;;AAEA;EAjBJ;AAkBQ;IACA;IACA;IACA;AAEA;IACA;IACA;IACA;AAEA;IACA;IACA;;;;ACpCR;EACI;;;AAGJ;EACI;EACA;EACA;EACA;;;AAGJ;EAEI,WFFY;EEGZ;;AAEA;EACI,WFJQ;;AEOZ;EATJ;IAUQ;;;AAGJ;EACI;;AAEA;EACI;EACA;EACA,WC7BI;ED8BJ,aCDY;;ADKpB;EACI;EACA;EACA;;;AAKJ;EACI;EACA;EACA;;AAEA;EACI;EACA;EACA;;AAEA;AAAA;EAEI;EACA;EACA;EACA,eJ1DR;EI2DQ;EACA;EACA,WCjED;EDkEC;;AAEA;AAAA;AAAA;EAEI;EACA;;AAOZ;EAEI;EACA;EACA;EACA,eJ9EJ;EI+EI","file":"style.css"}
 | 
			
		||||
							
								
								
									
										122
									
								
								styles/_theme.scss
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								styles/_theme.scss
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,122 @@
 | 
			
		|||
@use "tailwind/border-radius";
 | 
			
		||||
@use "tailwind/colors";
 | 
			
		||||
@use "tailwind/spacing";
 | 
			
		||||
@use "tailwind/transition";
 | 
			
		||||
@use "tailwind/text";
 | 
			
		||||
 | 
			
		||||
html {
 | 
			
		||||
    color-scheme: light dark;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* These need switchable client-side */
 | 
			
		||||
:root {
 | 
			
		||||
    /* Foreground */
 | 
			
		||||
    --color-fg-base: #{colors.$slate-700};
 | 
			
		||||
    --color-fg-deemphasized: #{colors.$slate-600};
 | 
			
		||||
    --color-fg-headings: #{colors.$slate-900};
 | 
			
		||||
 | 
			
		||||
    /* Background */
 | 
			
		||||
    --color-bg-base: #{colors.$slate-100};
 | 
			
		||||
    --color-bg-raised-1: #{colors.$slate-200};
 | 
			
		||||
    --color-bg-raised-2: #{colors.$slate-300};
 | 
			
		||||
    --color-bg-raised-3: #{colors.$slate-400};
 | 
			
		||||
 | 
			
		||||
    /* Borders */
 | 
			
		||||
    --color-bd-base: #{colors.$slate-300};
 | 
			
		||||
    --color-bd-highlighted-1: #{colors.$slate-500};
 | 
			
		||||
    --color-bd-highlighted-2: #{colors.$slate-600};
 | 
			
		||||
 | 
			
		||||
    @media (prefers-color-scheme: dark) {
 | 
			
		||||
        /* Foreground */
 | 
			
		||||
        --color-fg-base: #{colors.$slate-300};
 | 
			
		||||
        --color-fg-deemphasized: #{colors.$slate-400};
 | 
			
		||||
        --color-fg-headings: #{colors.$slate-100};
 | 
			
		||||
 | 
			
		||||
        /* Background */
 | 
			
		||||
        --color-bg-base: #{colors.$slate-900};
 | 
			
		||||
        --color-bg-raised-1: #{colors.$slate-800};
 | 
			
		||||
        --color-bg-raised-2: #{colors.$slate-700};
 | 
			
		||||
 | 
			
		||||
        /* Borders */
 | 
			
		||||
        --color-bd-base: #{colors.$slate-700};
 | 
			
		||||
        --color-bd-highlighted: #{colors.$slate-500};
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@mixin heading($disable-margin: false) {
 | 
			
		||||
    color: var(--color-fg-headings);
 | 
			
		||||
    font-weight: text.$font-weight-bold;
 | 
			
		||||
    @if $disable-margin {
 | 
			
		||||
        margin: 0;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@mixin heading2($disable-margin: false) {
 | 
			
		||||
    @include heading($disable-margin);
 | 
			
		||||
    font-size: text.$font-size-4xl;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@mixin heading3($disable-margin: false) {
 | 
			
		||||
    @include heading($disable-margin);
 | 
			
		||||
    font-size: text.$font-size-3xl;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@mixin heading4($disable-margin: false) {
 | 
			
		||||
    @include heading($disable-margin);
 | 
			
		||||
    font-size: text.$font-size-2xl;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@mixin surface($is-interactive: false, $is-raised: false) {
 | 
			
		||||
    border: 1px solid var(--color-bd-base);
 | 
			
		||||
    border-radius: border-radius.$br-xl;
 | 
			
		||||
    transition-duration: transition.$duration-default;
 | 
			
		||||
    transition-timing-function: transition.$timing-fn-default;
 | 
			
		||||
    transition-property: background-color, border-color;
 | 
			
		||||
 | 
			
		||||
    @if $is-raised {
 | 
			
		||||
        background-color: var(--color-bg-raised-1);
 | 
			
		||||
    } @else {
 | 
			
		||||
        background-color: unset;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @if $is-interactive {
 | 
			
		||||
        cursor: pointer;
 | 
			
		||||
 | 
			
		||||
        &:hover,
 | 
			
		||||
        &:focus-visible {
 | 
			
		||||
            border-color: var(--color-bd-highlighted-1);
 | 
			
		||||
            @if $is-raised {
 | 
			
		||||
                background-color: var(--color-bg-raised-2);
 | 
			
		||||
            } @else {
 | 
			
		||||
                background-color: var(--color-bg-raised-1);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        &.active- {
 | 
			
		||||
            border-color: var(--color-bd-highlighted-1);
 | 
			
		||||
            @if $is-raised {
 | 
			
		||||
                background-color: var(--color-bg-raised-2);
 | 
			
		||||
            } @else {
 | 
			
		||||
                background-color: var(--color-bg-raised-1);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            &:hover,
 | 
			
		||||
            &:focus-visible {
 | 
			
		||||
                border-color: var(--color-bd-highlighted-2);
 | 
			
		||||
                @if $is-raised {
 | 
			
		||||
                    background-color: var(--color-bg-raised-3);
 | 
			
		||||
                } @else {
 | 
			
		||||
                    background-color: var(--color-bg-raised-2);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@mixin button($is-raised: false) {
 | 
			
		||||
    @include surface(true, $is-raised);
 | 
			
		||||
    display: block;
 | 
			
		||||
    padding: spacing.s(1) spacing.s(3);
 | 
			
		||||
    font-size: text.$font-size-sm;
 | 
			
		||||
    color: unset;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										109
									
								
								styles/admin.scss
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								styles/admin.scss
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,109 @@
 | 
			
		|||
@use "theme";
 | 
			
		||||
@use "tailwind/spacing";
 | 
			
		||||
 | 
			
		||||
body.admin {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    max-width: spacing.$max-width-5xl;
 | 
			
		||||
    padding: spacing.s(16) spacing.s(8);
 | 
			
		||||
    margin: 0 auto;
 | 
			
		||||
    display: grid;
 | 
			
		||||
    grid-template-columns: spacing.s(64) auto;
 | 
			
		||||
    grid-template-rows: auto auto auto;
 | 
			
		||||
    grid-template-areas:
 | 
			
		||||
        "logo none"
 | 
			
		||||
        "sidebar main"
 | 
			
		||||
        "footer footer";
 | 
			
		||||
    gap: spacing.s(4);
 | 
			
		||||
 | 
			
		||||
    > h1 {
 | 
			
		||||
        @include theme.heading2(true);
 | 
			
		||||
        grid-area: logo;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    > aside[role="navigation"] {
 | 
			
		||||
        grid-area: sidebar;
 | 
			
		||||
 | 
			
		||||
        ul {
 | 
			
		||||
            list-style: none;
 | 
			
		||||
            margin: unset;
 | 
			
		||||
            padding: unset;
 | 
			
		||||
            display: flex;
 | 
			
		||||
            flex-direction: column;
 | 
			
		||||
            gap: spacing.s(2);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        a {
 | 
			
		||||
            display: block;
 | 
			
		||||
            padding: spacing.s(3) spacing.s(4);
 | 
			
		||||
            color: unset;
 | 
			
		||||
            text-decoration: unset;
 | 
			
		||||
            @include theme.surface(true, false);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    > main {
 | 
			
		||||
        grid-area: main;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    > footer {
 | 
			
		||||
        grid-area: footer;
 | 
			
		||||
        color: var(--color-fg-deemphasized);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body.admin-pastes {
 | 
			
		||||
    main > ul {
 | 
			
		||||
        list-style: none;
 | 
			
		||||
        margin: unset;
 | 
			
		||||
        padding: unset;
 | 
			
		||||
        display: flex;
 | 
			
		||||
        flex-direction: column;
 | 
			
		||||
        gap: spacing.s(6);
 | 
			
		||||
 | 
			
		||||
        li {
 | 
			
		||||
            display: grid;
 | 
			
		||||
            grid-template-columns: auto auto;
 | 
			
		||||
            grid-template-rows: auto auto;
 | 
			
		||||
            grid-template-areas:
 | 
			
		||||
                "title buttons"
 | 
			
		||||
                "timestamp buttons"
 | 
			
		||||
                "content content";
 | 
			
		||||
            gap: spacing.s(4);
 | 
			
		||||
 | 
			
		||||
            .title {
 | 
			
		||||
                grid-area: title;
 | 
			
		||||
                @include theme.heading4(true);
 | 
			
		||||
                text-decoration: none;
 | 
			
		||||
 | 
			
		||||
                &:hover,
 | 
			
		||||
                &:focus-visible {
 | 
			
		||||
                    text-decoration: underline;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            time {
 | 
			
		||||
                grid-area: timestamp;
 | 
			
		||||
                color: var(--color-fg-deemphasized);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .action-buttons {
 | 
			
		||||
                grid-area: buttons;
 | 
			
		||||
                display: flex;
 | 
			
		||||
                justify-content: end;
 | 
			
		||||
                align-items: end;
 | 
			
		||||
                gap: spacing.s(2);
 | 
			
		||||
 | 
			
		||||
                button {
 | 
			
		||||
                    @include theme.button(true);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .content {
 | 
			
		||||
                grid-area: content;
 | 
			
		||||
                @include theme.surface(false, true);
 | 
			
		||||
                padding: spacing.s(4);
 | 
			
		||||
                margin: unset;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										141
									
								
								styles/style.scss
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								styles/style.scss
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,141 @@
 | 
			
		|||
@use "theme";
 | 
			
		||||
@use "tailwind/border-radius";
 | 
			
		||||
@use "tailwind/spacing";
 | 
			
		||||
@use "tailwind/text";
 | 
			
		||||
 | 
			
		||||
* {
 | 
			
		||||
    box-sizing: border-box;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body {
 | 
			
		||||
    font-family: Inter, "Inter Variable", sans-serif;
 | 
			
		||||
    color: var(--color-fg-base);
 | 
			
		||||
    background-color: var(--color-bg-base);
 | 
			
		||||
    margin: 0 auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body:not(.admin) {
 | 
			
		||||
    // @apply flex flex-col gap-8 px-4 py-16 mx-auto max-w-4xl bg-slate-900 text-fg-base max-lg:py-8;
 | 
			
		||||
    max-width: spacing.$max-width-4xl;
 | 
			
		||||
    padding: spacing.s(16) spacing.s(8);
 | 
			
		||||
 | 
			
		||||
    &.wide {
 | 
			
		||||
        max-width: spacing.$max-width-6xl;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @media not all and (min-width: var(--max-width-lg)) {
 | 
			
		||||
        padding: spacing.s(8);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    header {
 | 
			
		||||
        margin-bottom: spacing.s(8);
 | 
			
		||||
 | 
			
		||||
        h1 {
 | 
			
		||||
            margin: 0;
 | 
			
		||||
            color: var(--color-fg-headings);
 | 
			
		||||
            font-size: text.$font-size-3xl;
 | 
			
		||||
            font-weight: text.$font-weight-extrabold;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    footer {
 | 
			
		||||
        color: var(--color-fg-deemphasized);
 | 
			
		||||
        margin-top: spacing.s(8);
 | 
			
		||||
        text-align: center;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body.paste {
 | 
			
		||||
    header {
 | 
			
		||||
        display: flex;
 | 
			
		||||
        justify-content: space-between;
 | 
			
		||||
        align-items: end;
 | 
			
		||||
 | 
			
		||||
        .action-buttons {
 | 
			
		||||
            display: flex;
 | 
			
		||||
            justify-content: end;
 | 
			
		||||
            gap: spacing.s(2);
 | 
			
		||||
 | 
			
		||||
            button,
 | 
			
		||||
            [role="button"] {
 | 
			
		||||
                background-color: var(--color-bg-raised-1);
 | 
			
		||||
                padding: spacing.s(1) spacing.s(3);
 | 
			
		||||
                border: 1px solid var(--color-bd-base);
 | 
			
		||||
                border-radius: border-radius.$br-lg;
 | 
			
		||||
                cursor: pointer;
 | 
			
		||||
                color: inherit;
 | 
			
		||||
                font-size: text.$font-size-sm;
 | 
			
		||||
                text-decoration: none;
 | 
			
		||||
 | 
			
		||||
                &:hover,
 | 
			
		||||
                &:focus-visible {
 | 
			
		||||
                    background-color: var(--color-bg-raised-2);
 | 
			
		||||
                    border-color: var(--color-bd-highlighted);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    main {
 | 
			
		||||
        pre {
 | 
			
		||||
            // pre class="p-4 border border-bd-base rounded-xl" { (r.content) }
 | 
			
		||||
            background-color: var(--color-bg-raised-1);
 | 
			
		||||
            padding: spacing.s(4);
 | 
			
		||||
            border: 1px solid var(--color-bd-base);
 | 
			
		||||
            border-radius: border-radius.$br-xl;
 | 
			
		||||
            margin: 0;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// body.admin-pastes {
 | 
			
		||||
//     main form {
 | 
			
		||||
//         input[type="text"],
 | 
			
		||||
//         textarea,
 | 
			
		||||
//         input[type="submit"] {
 | 
			
		||||
//             padding: spacing.s(2) spacing.s(3);
 | 
			
		||||
//             border-radius: border-radius.$br-lg;
 | 
			
		||||
//             background-color: var(--color-bg-raised-1);
 | 
			
		||||
//             border: 1px solid var(--color-bd-base);
 | 
			
		||||
//             font-size: inherit;
 | 
			
		||||
// 
 | 
			
		||||
//             &:hover,
 | 
			
		||||
//             &:focus-visible {
 | 
			
		||||
//                 background-color: var(--color-bg-raised-2);
 | 
			
		||||
//                 border-color: var(--color-bd-highlighted);
 | 
			
		||||
//             }
 | 
			
		||||
//         }
 | 
			
		||||
// 
 | 
			
		||||
//         input[type="text"],
 | 
			
		||||
//         textarea {
 | 
			
		||||
//             width: 100%;
 | 
			
		||||
//             margin-bottom: spacing.s(4);
 | 
			
		||||
//         }
 | 
			
		||||
// 
 | 
			
		||||
//         textarea {
 | 
			
		||||
//             min-height: spacing.s(32);
 | 
			
		||||
//         }
 | 
			
		||||
// 
 | 
			
		||||
//         input[type="submit"] {
 | 
			
		||||
//             cursor: pointer;
 | 
			
		||||
//             margin-left: auto;
 | 
			
		||||
//             display: block;
 | 
			
		||||
//         }
 | 
			
		||||
//     }
 | 
			
		||||
// }
 | 
			
		||||
 | 
			
		||||
a.tl {
 | 
			
		||||
    // @apply underline hover:no-underline hover:text-white focus-visible:no-underline focus-visible:text-white;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
table {
 | 
			
		||||
    // @apply table-auto border-collapse border border-bd-base;
 | 
			
		||||
 | 
			
		||||
    thead tr th {
 | 
			
		||||
        // @apply border border-bd-base px-2 py-1 text-left;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    tbody tr td {
 | 
			
		||||
        // @apply border border-bd-base px-2 py-1;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										10
									
								
								styles/tailwind/_border-radius.scss
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								styles/tailwind/_border-radius.scss
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,10 @@
 | 
			
		|||
/* https://github.com/tailwindlabs/tailwindcss/blob/ac6d4a6e8e9ae7ca197bf98d933ae2f205be3635/packages/tailwindcss/src/compat/default-theme.ts#L162 */
 | 
			
		||||
$br-none: 0px;
 | 
			
		||||
$br-sm: 0.125rem;
 | 
			
		||||
$br-default: 0.25rem;
 | 
			
		||||
$br-md: 0.375rem;
 | 
			
		||||
$br-lg: 0.5rem;
 | 
			
		||||
$br-xl: 0.75rem;
 | 
			
		||||
$br-2xl: 1rem;
 | 
			
		||||
$br-3xl: 1.5rem;
 | 
			
		||||
$br-full: 9999px;
 | 
			
		||||
							
								
								
									
										12
									
								
								styles/tailwind/_colors.scss
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								styles/tailwind/_colors.scss
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,12 @@
 | 
			
		|||
/* Tailwind palette */
 | 
			
		||||
$slate-50: #f8fafc;
 | 
			
		||||
$slate-100: #f1f5f9;
 | 
			
		||||
$slate-200: #e2e8f0;
 | 
			
		||||
$slate-300: #cbd5e1;
 | 
			
		||||
$slate-400: #94a3b8;
 | 
			
		||||
$slate-500: #64748b;
 | 
			
		||||
$slate-600: #475569;
 | 
			
		||||
$slate-700: #334155;
 | 
			
		||||
$slate-800: #1e293b;
 | 
			
		||||
$slate-900: #0f172a;
 | 
			
		||||
$slate-950: #020617;
 | 
			
		||||
							
								
								
									
										0
									
								
								styles/tailwind/_max-width.scss
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								styles/tailwind/_max-width.scss
									
										
									
									
									
										Normal file
									
								
							
							
								
								
									
										6
									
								
								styles/tailwind/_screens.scss
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								styles/tailwind/_screens.scss
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,6 @@
 | 
			
		|||
/* https://github.com/tailwindlabs/tailwindcss/blob/ac6d4a6e8e9ae7ca197bf98d933ae2f205be3635/packages/tailwindcss/src/compat/default-theme.ts#L885 */
 | 
			
		||||
$screen-sm: 640px;
 | 
			
		||||
$screen-md: 768px;
 | 
			
		||||
$screen-lg: 1024px;
 | 
			
		||||
$screen-xl: 1280px;
 | 
			
		||||
$screen-2xl: 1536px;
 | 
			
		||||
							
								
								
									
										25
									
								
								styles/tailwind/_spacing.scss
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								styles/tailwind/_spacing.scss
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,25 @@
 | 
			
		|||
/* Yes, it's that simple.
 | 
			
		||||
 * https://tailwindcss.com/docs/customizing-spacing#default-spacing-scale
 | 
			
		||||
 * https://github.com/tailwindlabs/tailwindcss/blob/ac6d4a6e8e9ae7ca197bf98d933ae2f205be3635/packages/tailwindcss/src/compat/default-theme.ts#L909 */
 | 
			
		||||
@function s($n) {
 | 
			
		||||
  @return 0.25rem * $n;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* https://github.com/tailwindlabs/tailwindcss/blob/ac6d4a6e8e9ae7ca197bf98d933ae2f205be3635/packages/tailwindcss/src/compat/default-theme.ts#L715 */
 | 
			
		||||
$max-width-none: none;
 | 
			
		||||
$max-width-xs: 20rem;
 | 
			
		||||
$max-width-sm: 24rem;
 | 
			
		||||
$max-width-md: 28rem;
 | 
			
		||||
$max-width-lg: 32rem;
 | 
			
		||||
$max-width-xl: 36rem;
 | 
			
		||||
$max-width-2xl: 42rem;
 | 
			
		||||
$max-width-3xl: 48rem;
 | 
			
		||||
$max-width-4xl: 56rem;
 | 
			
		||||
$max-width-5xl: 64rem;
 | 
			
		||||
$max-width-6xl: 72rem;
 | 
			
		||||
$max-width-7xl: 80rem;
 | 
			
		||||
$max-width-full: 100%;
 | 
			
		||||
$max-width-min: min-content;
 | 
			
		||||
$max-width-max: max-content;
 | 
			
		||||
$max-width-fit: fit-content;
 | 
			
		||||
$max-width-prose: 65ch;
 | 
			
		||||
							
								
								
									
										37
									
								
								styles/tailwind/_text.scss
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								styles/tailwind/_text.scss
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,37 @@
 | 
			
		|||
$font-size-xs: 0.75rem;
 | 
			
		||||
$font-size-sm: 0.875rem;
 | 
			
		||||
$font-size-base: 1rem;
 | 
			
		||||
$font-size-lg: 1.125rem;
 | 
			
		||||
$font-size-xl: 1.25rem;
 | 
			
		||||
$font-size-2xl: 1.5rem;
 | 
			
		||||
$font-size-3xl: 1.875rem;
 | 
			
		||||
$font-size-4xl: 2.25rem;
 | 
			
		||||
$font-size-5xl: 3rem;
 | 
			
		||||
$font-size-6xl: 3.75rem;
 | 
			
		||||
$font-size-7xl: 4.5rem;
 | 
			
		||||
$font-size-8xl: 6rem;
 | 
			
		||||
$font-size-9xl: 8rem;
 | 
			
		||||
 | 
			
		||||
$line-height-xs: 1rem;
 | 
			
		||||
$line-height-sm: 1.25rem;
 | 
			
		||||
$line-height-base: 1.5rem;
 | 
			
		||||
$line-height-lg: 1.75rem;
 | 
			
		||||
$line-height-xl: 1.75rem;
 | 
			
		||||
$line-height-2xl: 2rem;
 | 
			
		||||
$line-height-3xl: 2.25rem;
 | 
			
		||||
$line-height-4xl: 2.5rem;
 | 
			
		||||
$line-height-5xl: 1;
 | 
			
		||||
$line-height-6xl: 1;
 | 
			
		||||
$line-height-7xl: 1;
 | 
			
		||||
$line-height-8xl: 1;
 | 
			
		||||
$line-height-9xl: 1;
 | 
			
		||||
 | 
			
		||||
$font-weight-thin: 100;
 | 
			
		||||
$font-weight-extralight: 200;
 | 
			
		||||
$font-weight-light: 300;
 | 
			
		||||
$font-weight-normal: 400;
 | 
			
		||||
$font-weight-medium: 500;
 | 
			
		||||
$font-weight-semibold: 600;
 | 
			
		||||
$font-weight-bold: 700;
 | 
			
		||||
$font-weight-extrabold: 800;
 | 
			
		||||
$font-weight-black: 900;
 | 
			
		||||
							
								
								
									
										7
									
								
								styles/tailwind/_transition.scss
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								styles/tailwind/_transition.scss
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
$timing-fn-default: cubic-bezier(0.4, 0, 0.2, 1);
 | 
			
		||||
$timing-fn-linear: linear;
 | 
			
		||||
$timing-fn-in: cubic-bezier(0.4, 0, 1, 1);
 | 
			
		||||
$timing-fn-out: cubic-bezier(0, 0, 0.2, 1);
 | 
			
		||||
$timing-fn-in-out: cubic-bezier(0.4, 0, 0.2, 1);
 | 
			
		||||
 | 
			
		||||
$duration-default: 150ms;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,34 +0,0 @@
 | 
			
		|||
/** @type {import("tailwindcss/colors")} */
 | 
			
		||||
const colors = require("tailwindcss/colors");
 | 
			
		||||
 | 
			
		||||
/** @type {import("tailwindcss").Config} */
 | 
			
		||||
module.exports = {
 | 
			
		||||
  content: ["./src/app/pages/**/*.rs"],
 | 
			
		||||
  theme: {
 | 
			
		||||
    fontFamily: {
 | 
			
		||||
      sans: ["sans-serif"],
 | 
			
		||||
      mono: ["monospace"],
 | 
			
		||||
    },
 | 
			
		||||
    extend: {
 | 
			
		||||
      colors: {
 | 
			
		||||
        fg: {
 | 
			
		||||
          base: colors.slate[300],
 | 
			
		||||
          deemphasized: colors.slate[400],
 | 
			
		||||
          headings: colors.white,
 | 
			
		||||
        },
 | 
			
		||||
        bg: {
 | 
			
		||||
          base: colors.slate[900],
 | 
			
		||||
          raised: {
 | 
			
		||||
            // backgrounds
 | 
			
		||||
            1: colors.slate[800],
 | 
			
		||||
            2: colors.slate[700],
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        bd: {
 | 
			
		||||
          base: colors.slate[700],
 | 
			
		||||
          highlighted: colors.slate[500],
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										31
									
								
								templates/admin/base.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								templates/admin/base.html
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,31 @@
 | 
			
		|||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
    <head>
 | 
			
		||||
        <meta charset="UTF-8" />
 | 
			
		||||
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 | 
			
		||||
        <title>{% block title %}{{title}}{% endblock %} | NCPN</title>
 | 
			
		||||
        <link rel="stylesheet" href="/style.css" />
 | 
			
		||||
        <link rel="stylesheet" href="/admin.css" />
 | 
			
		||||
        <script src="/script.js" defer></script>
 | 
			
		||||
        <noscript>
 | 
			
		||||
            <style>
 | 
			
		||||
                .jsonly {
 | 
			
		||||
                    display: none;
 | 
			
		||||
                }
 | 
			
		||||
            </style>
 | 
			
		||||
        </noscript>
 | 
			
		||||
        {% block head %}{% endblock %}
 | 
			
		||||
    </head>
 | 
			
		||||
    <body class="admin {% block bodyClass %}{% endblock %}">
 | 
			
		||||
        <h1>NCPN Admin</h1>
 | 
			
		||||
        <aside role="navigation">
 | 
			
		||||
            <ul>
 | 
			
		||||
                <li><a href="/admin/files">Files</a></li>
 | 
			
		||||
                <li><a href="/admin/links">Links</a></li>
 | 
			
		||||
                <li><a class="active" href="/admin/pastes">Pastes</a></li>
 | 
			
		||||
            </ul>
 | 
			
		||||
        </aside>
 | 
			
		||||
        <main>{% block main %}{% endblock %}</main>
 | 
			
		||||
        <footer>Running NCPN v?.??</footer>
 | 
			
		||||
    </body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										18
									
								
								templates/admin/paste-new.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								templates/admin/paste-new.html
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,18 @@
 | 
			
		|||
{% extends "../base.html" %}
 | 
			
		||||
 | 
			
		||||
{% block title %}Create Paste{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block bodyClass %}admin-pastes{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block header %}
 | 
			
		||||
<h1>Create Paste</h1>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block main %}
 | 
			
		||||
<form method="post" action="/admin/pastes">
 | 
			
		||||
    <input type="text" name="title" placeholder="Paste title" required value="{{paste_title}}" />
 | 
			
		||||
    <textarea name="content" placeholder="Paste content" required>{{paste_content}}</textarea>
 | 
			
		||||
    <input type="submit" value="Create" />
 | 
			
		||||
</form>
 | 
			
		||||
{% call super() %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
							
								
								
									
										21
									
								
								templates/admin/pastes.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								templates/admin/pastes.html
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,21 @@
 | 
			
		|||
{% extends "base.html" %}
 | 
			
		||||
 | 
			
		||||
{% block title %}Pastes{% endblock %}
 | 
			
		||||
{% block bodyClass %}admin-pastes{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block main %}
 | 
			
		||||
<ul>
 | 
			
		||||
    {% for paste in pastes %}
 | 
			
		||||
    <li>
 | 
			
		||||
        <a class="title" href="/p/{{ paste.id }}">{{ paste.title|e }}</a>
 | 
			
		||||
        <time datetime="{{ paste.timestamp }}">{{ paste.timestamp }}</time>
 | 
			
		||||
        <div class="action-buttons">
 | 
			
		||||
            <button>Edit</button>
 | 
			
		||||
            <button>Delete</button>
 | 
			
		||||
        </div>
 | 
			
		||||
        <pre class="content">{{ paste.content|e }}</pre>
 | 
			
		||||
    </li>
 | 
			
		||||
    {% endfor %}
 | 
			
		||||
</ul>
 | 
			
		||||
{% call super() %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
							
								
								
									
										23
									
								
								templates/base.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								templates/base.html
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,23 @@
 | 
			
		|||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
    <head>
 | 
			
		||||
        <meta charset="UTF-8" />
 | 
			
		||||
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 | 
			
		||||
        <title>{% block title %}{{title}}{% endblock %} | NCPN</title>
 | 
			
		||||
        <link rel="stylesheet" href="/style.css" />
 | 
			
		||||
        <script src="/script.js" defer></script>
 | 
			
		||||
        <noscript>
 | 
			
		||||
            <style>
 | 
			
		||||
                .jsonly {
 | 
			
		||||
                    display: none;
 | 
			
		||||
                }
 | 
			
		||||
            </style>
 | 
			
		||||
        </noscript>
 | 
			
		||||
        {% block head %}{% endblock %}
 | 
			
		||||
    </head>
 | 
			
		||||
    <body class="{% block bodyClass %}{% endblock %}">
 | 
			
		||||
        <header>{% block header %}{% endblock %}</header>
 | 
			
		||||
        <main>{% block main %}{% endblock %}</main>
 | 
			
		||||
        <footer>Running NCPN v?.??</footer>
 | 
			
		||||
    </body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										16
									
								
								templates/paste.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								templates/paste.html
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,16 @@
 | 
			
		|||
{% extends "base.html" %}
 | 
			
		||||
 | 
			
		||||
{% block bodyClass %}paste{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block header %}
 | 
			
		||||
<h1>{{title}}</h1>
 | 
			
		||||
<div class="action-buttons">
 | 
			
		||||
    <a role="button" href="/p/{{id}}/raw">Raw</a>
 | 
			
		||||
    <button class="jsonly" onclick="copyPasteContent()">Copy</button>
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block main %}
 | 
			
		||||
<pre id="paste-content">{{content}}</pre>
 | 
			
		||||
{% call super() %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
							
								
								
									
										1
									
								
								templates/test.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								templates/test.html
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
Hello, {{ name }}!
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue