Edit posts
Use fix routes for user profile edit handles to make it easier when user decides to change his username
This commit is contained in:
16
.sqlx/query-790175eabe78857d16f1890ef4a1cd71f3a9c5f3e98a7c00553196f7a48d3bc3.json
generated
Normal file
16
.sqlx/query-790175eabe78857d16f1890ef4a1cd71f3a9c5f3e98a7c00553196f7a48d3bc3.json
generated
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE posts\n SET title = $1, content = $2 WHERE post_id = $3\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "790175eabe78857d16f1890ef4a1cd71f3a9c5f3e98a7c00553196f7a48d3bc3"
|
||||
}
|
||||
22
.sqlx/query-84fcada696e1be5db55ef276e120ffef9adf7f5a4f5c4d5975b85e008e15620b.json
generated
Normal file
22
.sqlx/query-84fcada696e1be5db55ef276e120ffef9adf7f5a4f5c4d5975b85e008e15620b.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT author_id FROM posts WHERE post_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "author_id",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "84fcada696e1be5db55ef276e120ffef9adf7f5a4f5c4d5975b85e008e15620b"
|
||||
}
|
||||
@@ -34,14 +34,12 @@
|
||||
--color-green-800: oklch(44.8% 0.119 151.328);
|
||||
--color-blue-50: oklch(97% 0.014 254.604);
|
||||
--color-blue-100: oklch(93.2% 0.032 255.585);
|
||||
--color-blue-200: oklch(88.2% 0.059 254.128);
|
||||
--color-blue-300: oklch(80.9% 0.105 251.813);
|
||||
--color-blue-500: oklch(62.3% 0.214 259.815);
|
||||
--color-blue-600: oklch(54.6% 0.245 262.881);
|
||||
--color-blue-700: oklch(48.8% 0.243 264.376);
|
||||
--color-blue-800: oklch(42.4% 0.199 265.638);
|
||||
--color-blue-900: oklch(37.9% 0.146 265.522);
|
||||
--color-indigo-50: oklch(96.2% 0.018 272.314);
|
||||
--color-indigo-500: oklch(58.5% 0.233 277.117);
|
||||
--color-indigo-600: oklch(51.1% 0.262 276.966);
|
||||
--color-indigo-700: oklch(45.7% 0.24 277.023);
|
||||
@@ -294,9 +292,6 @@
|
||||
max-width: 96rem;
|
||||
}
|
||||
}
|
||||
.mx-2 {
|
||||
margin-inline: calc(var(--spacing) * 2);
|
||||
}
|
||||
.mx-auto {
|
||||
margin-inline: auto;
|
||||
}
|
||||
@@ -321,9 +316,6 @@
|
||||
.mt-8 {
|
||||
margin-top: calc(var(--spacing) * 8);
|
||||
}
|
||||
.mt-12 {
|
||||
margin-top: calc(var(--spacing) * 12);
|
||||
}
|
||||
.mt-auto {
|
||||
margin-top: auto;
|
||||
}
|
||||
@@ -641,10 +633,6 @@
|
||||
border-bottom-style: var(--tw-border-style);
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
.border-b-2 {
|
||||
border-bottom-style: var(--tw-border-style);
|
||||
border-bottom-width: 2px;
|
||||
}
|
||||
.border-l-4 {
|
||||
border-left-style: var(--tw-border-style);
|
||||
border-left-width: 4px;
|
||||
@@ -810,6 +798,9 @@
|
||||
.py-1 {
|
||||
padding-block: calc(var(--spacing) * 1);
|
||||
}
|
||||
.py-1\.5 {
|
||||
padding-block: calc(var(--spacing) * 1.5);
|
||||
}
|
||||
.py-2 {
|
||||
padding-block: calc(var(--spacing) * 2);
|
||||
}
|
||||
@@ -837,9 +828,6 @@
|
||||
.pb-4 {
|
||||
padding-bottom: calc(var(--spacing) * 4);
|
||||
}
|
||||
.pb-8 {
|
||||
padding-bottom: calc(var(--spacing) * 8);
|
||||
}
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
@@ -1295,6 +1283,11 @@
|
||||
margin-inline: calc(var(--spacing) * 0);
|
||||
}
|
||||
}
|
||||
.sm\:mt-0 {
|
||||
@media (width >= 40rem) {
|
||||
margin-top: calc(var(--spacing) * 0);
|
||||
}
|
||||
}
|
||||
.sm\:inline {
|
||||
@media (width >= 40rem) {
|
||||
display: inline;
|
||||
@@ -1394,11 +1387,6 @@
|
||||
line-height: var(--tw-leading, var(--text-4xl--line-height));
|
||||
}
|
||||
}
|
||||
.lg\:col-span-2 {
|
||||
@media (width >= 64rem) {
|
||||
grid-column: span 2 / span 2;
|
||||
}
|
||||
}
|
||||
.lg\:ml-0 {
|
||||
@media (width >= 64rem) {
|
||||
margin-left: calc(var(--spacing) * 0);
|
||||
@@ -1892,7 +1880,7 @@
|
||||
margin-bottom: calc(var(--spacing) * 3);
|
||||
border-bottom-style: var(--tw-border-style);
|
||||
border-bottom-width: 2px;
|
||||
border-color: var(--color-gray-100);
|
||||
border-color: var(--color-gray-200);
|
||||
padding-bottom: calc(var(--spacing) * 2);
|
||||
}
|
||||
.prose-compact h2 {
|
||||
@@ -1900,7 +1888,7 @@
|
||||
margin-bottom: calc(var(--spacing) * 3);
|
||||
border-bottom-style: var(--tw-border-style);
|
||||
border-bottom-width: 2px;
|
||||
border-color: var(--color-gray-100);
|
||||
border-color: var(--color-gray-200);
|
||||
padding-bottom: calc(var(--spacing) * 2);
|
||||
--tw-font-weight: var(--font-weight-semibold);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
|
||||
@@ -14,13 +14,9 @@ impl PostEntry {
|
||||
self.published_at.format("%B %d, %Y").to_string()
|
||||
}
|
||||
|
||||
pub fn to_html(self) -> Result<Self, anyhow::Error> {
|
||||
pub fn to_html(&self) -> anyhow::Result<String> {
|
||||
match markdown::to_html_with_options(&self.content, &markdown::Options::gfm()) {
|
||||
Ok(mut content) => {
|
||||
content = content.replace("<table>", r#"<div class="table-wrapper"><table>"#);
|
||||
content = content.replace("</table>", r#"</table></div>"#);
|
||||
Ok(Self { content, ..self })
|
||||
}
|
||||
Ok(content) => Ok(content),
|
||||
Err(e) => anyhow::bail!(e),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use crate::routes::{COMMENTS_PER_PAGE, get_max_page};
|
||||
use crate::templates::PostsPageDashboardTemplate;
|
||||
use crate::authentication::AuthenticatedUser;
|
||||
use crate::routes::{COMMENTS_PER_PAGE, Query, get_max_page};
|
||||
use crate::session_state::TypedSession;
|
||||
use crate::templates::{ErrorTemplate, MessageTemplate, PostsPageDashboardTemplate};
|
||||
use crate::{
|
||||
domain::PostEntry,
|
||||
routes::{
|
||||
AppError, Path, Query, get_comments_count_for_post, get_comments_page_for_post,
|
||||
not_found_html,
|
||||
AppError, Path, get_comments_count_for_post, get_comments_page_for_post, not_found_html,
|
||||
},
|
||||
startup::AppState,
|
||||
templates::{HtmlTemplate, PostListTemplate, PostTemplate, PostsTemplate},
|
||||
@@ -12,6 +13,7 @@ use crate::{
|
||||
use anyhow::Context;
|
||||
use askama::Template;
|
||||
use axum::{
|
||||
Extension, Form,
|
||||
extract::State,
|
||||
response::{Html, IntoResponse, Redirect, Response},
|
||||
};
|
||||
@@ -118,17 +120,65 @@ pub async fn get_posts_count(connection_pool: &PgPool) -> Result<i64, sqlx::Erro
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct PostParams {
|
||||
pub struct EditPostForm {
|
||||
pub title: String,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[tracing::instrument(name = "Editing post", skip_all, fields(post_id = %post_id))]
|
||||
pub async fn update_post(
|
||||
State(AppState {
|
||||
connection_pool, ..
|
||||
}): State<AppState>,
|
||||
Extension(AuthenticatedUser { user_id, .. }): Extension<AuthenticatedUser>,
|
||||
Path(post_id): Path<Uuid>,
|
||||
Form(form): Form<EditPostForm>,
|
||||
) -> Result<Response, AppError> {
|
||||
let record = sqlx::query!("SELECT author_id FROM posts WHERE post_id = $1", post_id)
|
||||
.fetch_optional(&connection_pool)
|
||||
.await
|
||||
.context("Could not fetch post author.")?;
|
||||
match record {
|
||||
None => Ok(HtmlTemplate(ErrorTemplate::NotFound).into_response()),
|
||||
Some(record) if record.author_id == user_id => {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE posts
|
||||
SET title = $1, content = $2 WHERE post_id = $3
|
||||
",
|
||||
form.title,
|
||||
form.content,
|
||||
post_id
|
||||
)
|
||||
.execute(&connection_pool)
|
||||
.await
|
||||
.context("Could not update post")?;
|
||||
Ok(HtmlTemplate(MessageTemplate::success(
|
||||
"Your changes have been saved.".into(),
|
||||
))
|
||||
.into_response())
|
||||
}
|
||||
_ => Ok(HtmlTemplate(ErrorTemplate::Forbidden).into_response()),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct OriginQueryParam {
|
||||
origin: Option<Uuid>,
|
||||
}
|
||||
|
||||
#[tracing::instrument(name = "Fetching post from database", skip(connection_pool, origin))]
|
||||
#[tracing::instrument(
|
||||
name = "Fetching post from database",
|
||||
skip(connection_pool, origin, session)
|
||||
)]
|
||||
|
||||
pub async fn see_post(
|
||||
session: TypedSession,
|
||||
State(AppState {
|
||||
connection_pool, ..
|
||||
}): State<AppState>,
|
||||
Path(post_id): Path<Uuid>,
|
||||
Query(PostParams { origin }): Query<PostParams>,
|
||||
Query(OriginQueryParam { origin }): Query<OriginQueryParam>,
|
||||
) -> Result<Response, AppError> {
|
||||
if let Some(origin) = origin {
|
||||
mark_email_as_opened(&connection_pool, origin).await?;
|
||||
@@ -140,7 +190,7 @@ pub async fn see_post(
|
||||
.context(format!("Failed to fetch post #{}", post_id))
|
||||
.map_err(AppError::unexpected_page)?
|
||||
{
|
||||
let post = post
|
||||
let post_html = post
|
||||
.to_html()
|
||||
.context("Could not render markdown with extension.")?;
|
||||
let current_page = 1;
|
||||
@@ -152,13 +202,19 @@ pub async fn see_post(
|
||||
.await
|
||||
.context("Failed to fetch latest comments")?;
|
||||
let idempotency_key = Uuid::new_v4().to_string();
|
||||
let session_username = session
|
||||
.get_username()
|
||||
.await
|
||||
.context("Could not check for session username")?;
|
||||
let template = HtmlTemplate(PostTemplate {
|
||||
post,
|
||||
post_html,
|
||||
comments,
|
||||
idempotency_key,
|
||||
current_page,
|
||||
max_page,
|
||||
comments_count,
|
||||
session_username,
|
||||
});
|
||||
Ok(template.into_response())
|
||||
} else {
|
||||
|
||||
@@ -19,21 +19,12 @@ use secrecy::{ExposeSecret, SecretString};
|
||||
use sqlx::PgPool;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub async fn get_user_edit(
|
||||
Path(username): Path<String>,
|
||||
Extension(AuthenticatedUser {
|
||||
user_id,
|
||||
username: session_username,
|
||||
..
|
||||
}): Extension<AuthenticatedUser>,
|
||||
pub async fn user_edit_form(
|
||||
Extension(AuthenticatedUser { user_id, .. }): Extension<AuthenticatedUser>,
|
||||
State(AppState {
|
||||
connection_pool, ..
|
||||
}): State<AppState>,
|
||||
) -> Result<Response, AppError> {
|
||||
if username != session_username {
|
||||
let template = HtmlTemplate(ErrorTemplate::Forbidden);
|
||||
return Ok(template.into_response());
|
||||
}
|
||||
let user = sqlx::query_as!(
|
||||
UserEntry,
|
||||
r#"
|
||||
@@ -52,25 +43,26 @@ pub async fn get_user_edit(
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct EditProfileForm {
|
||||
user_id: Uuid,
|
||||
username: String,
|
||||
full_name: String,
|
||||
bio: String,
|
||||
}
|
||||
|
||||
pub async fn put_user_edit(
|
||||
#[tracing::instrument(name = "Updating user profile", skip_all, fields(user_id = %form.user_id))]
|
||||
pub async fn update_user(
|
||||
State(AppState {
|
||||
connection_pool, ..
|
||||
}): State<AppState>,
|
||||
session: TypedSession,
|
||||
Extension(AuthenticatedUser {
|
||||
user_id,
|
||||
user_id: session_user_id,
|
||||
username: session_username,
|
||||
..
|
||||
}): Extension<AuthenticatedUser>,
|
||||
Path(username): Path<String>,
|
||||
Form(form): Form<EditProfileForm>,
|
||||
) -> Result<Response, AppError> {
|
||||
if username != session_username {
|
||||
if form.user_id != session_user_id {
|
||||
let template = HtmlTemplate(ErrorTemplate::Forbidden);
|
||||
return Ok(template.into_response());
|
||||
}
|
||||
@@ -101,7 +93,7 @@ pub async fn put_user_edit(
|
||||
updated_username,
|
||||
updated_full_name,
|
||||
bio,
|
||||
user_id
|
||||
form.user_id
|
||||
)
|
||||
.execute(&connection_pool)
|
||||
.await
|
||||
|
||||
@@ -7,7 +7,7 @@ use axum::{
|
||||
http::Request,
|
||||
middleware,
|
||||
response::{IntoResponse, Response},
|
||||
routing::{delete, get, post},
|
||||
routing::{delete, get, post, put},
|
||||
};
|
||||
use reqwest::{StatusCode, header};
|
||||
use sqlx::{PgPool, postgres::PgPoolOptions};
|
||||
@@ -99,11 +99,9 @@ pub fn app(
|
||||
.route("/password", post(change_password))
|
||||
.route("/newsletters", post(publish_newsletter))
|
||||
.route("/posts", post(create_post))
|
||||
.route("/posts/{post_id}", put(update_post))
|
||||
.route("/logout", get(logout))
|
||||
.route(
|
||||
"/users/{username}/edit",
|
||||
get(get_user_edit).put(put_user_edit),
|
||||
)
|
||||
.route("/users/edit", get(user_edit_form).put(update_user))
|
||||
.nest("/admin", admin_routes)
|
||||
.layer(middleware::from_fn(require_auth));
|
||||
Router::new()
|
||||
|
||||
@@ -121,11 +121,13 @@ pub struct PostListTemplate {
|
||||
#[template(path = "posts/page.html")]
|
||||
pub struct PostTemplate {
|
||||
pub post: PostEntry,
|
||||
pub post_html: String,
|
||||
pub idempotency_key: String,
|
||||
pub comments: Vec<CommentEntry>,
|
||||
pub current_page: i64,
|
||||
pub max_page: i64,
|
||||
pub comments_count: i64,
|
||||
pub session_username: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
|
||||
@@ -1,150 +1,151 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@plugin "@tailwindcss/typography";
|
||||
|
||||
@layer utilities {
|
||||
.htmx-indicator {
|
||||
@apply hidden;
|
||||
}
|
||||
.htmx-indicator {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
.htmx-request .htmx-indicator {
|
||||
@apply inline-flex items-center ml-2;
|
||||
}
|
||||
.htmx-request .htmx-indicator {
|
||||
@apply inline-flex items-center ml-2;
|
||||
}
|
||||
|
||||
#load-more .htmx-indicator {
|
||||
@apply block;
|
||||
}
|
||||
#load-more .htmx-indicator {
|
||||
@apply block;
|
||||
}
|
||||
|
||||
.htmx-request .continue-text {
|
||||
@apply hidden;
|
||||
}
|
||||
.htmx-request .continue-text {
|
||||
@apply hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.prose-compact {
|
||||
@apply prose prose-slate max-w-none;
|
||||
.prose-compact {
|
||||
@apply prose prose-slate max-w-none;
|
||||
|
||||
--tw-prose-body: theme(colors.gray.700);
|
||||
--tw-prose-headings: theme(colors.gray.900);
|
||||
--tw-prose-links: theme(colors.blue.600);
|
||||
--tw-prose-code: theme(colors.gray.800);
|
||||
}
|
||||
--tw-prose-body: theme(colors.gray.700);
|
||||
--tw-prose-headings: theme(colors.gray.900);
|
||||
--tw-prose-links: theme(colors.blue.600);
|
||||
--tw-prose-code: theme(colors.gray.800);
|
||||
}
|
||||
|
||||
.prose-compact p {
|
||||
@apply mb-2 mt-0;
|
||||
}
|
||||
.prose-compact p {
|
||||
@apply mb-2 mt-0;
|
||||
}
|
||||
|
||||
.prose-compact h1 {
|
||||
@apply pb-2 mb-3 border-b-2 border-gray-100;
|
||||
}
|
||||
.prose-compact h1 {
|
||||
@apply pb-2 mb-3 border-b-2 border-gray-200;
|
||||
}
|
||||
|
||||
.prose-compact h2 {
|
||||
@apply mt-4 pb-2 mb-3 border-b-2 border-gray-100 font-semibold;
|
||||
}
|
||||
.prose-compact h2 {
|
||||
@apply mt-4 pb-2 mb-3 border-b-2 border-gray-200 font-semibold;
|
||||
}
|
||||
|
||||
.prose-compact h3 {
|
||||
@apply mt-3 mb-1;
|
||||
}
|
||||
.prose-compact h3 {
|
||||
@apply mt-3 mb-1;
|
||||
}
|
||||
|
||||
.prose-compact h4,
|
||||
.prose-compact h5,
|
||||
.prose-compact h6 {
|
||||
@apply mt-2 mb-1;
|
||||
}
|
||||
.prose-compact h4,
|
||||
.prose-compact h5,
|
||||
.prose-compact h6 {
|
||||
@apply mt-2 mb-1;
|
||||
}
|
||||
|
||||
.prose-compact ul,
|
||||
.prose-compact ol {
|
||||
@apply my-2 space-y-0;
|
||||
}
|
||||
.prose-compact ul,
|
||||
.prose-compact ol {
|
||||
@apply my-2 space-y-0;
|
||||
}
|
||||
|
||||
.prose-compact li {
|
||||
@apply my-0;
|
||||
}
|
||||
.prose-compact li {
|
||||
@apply my-0;
|
||||
}
|
||||
|
||||
.prose-compact blockquote {
|
||||
@apply my-3 py-2;
|
||||
}
|
||||
.prose-compact blockquote {
|
||||
@apply my-3 py-2;
|
||||
}
|
||||
|
||||
.prose-compact img {
|
||||
@apply m-0 align-top;
|
||||
}
|
||||
.prose-compact img {
|
||||
@apply m-0 align-top;
|
||||
}
|
||||
|
||||
.prose-compact a:has(img) {
|
||||
@apply no-underline border-0 inline-block align-top;
|
||||
}
|
||||
.prose-compact a:has(img) {
|
||||
@apply no-underline border-0 inline-block align-top;
|
||||
}
|
||||
|
||||
.prose-compact a img {
|
||||
@apply inline-block align-top;
|
||||
}
|
||||
.prose-compact a img {
|
||||
@apply inline-block align-top;
|
||||
}
|
||||
|
||||
.prose-compact :not(pre) > code {
|
||||
@apply bg-gray-100 text-gray-800 px-1.5 py-0.5 rounded text-sm font-mono font-normal;
|
||||
}
|
||||
.prose-compact :not(pre) > code {
|
||||
@apply bg-gray-100 text-gray-800 px-1.5 py-0.5 rounded text-sm font-mono font-normal;
|
||||
}
|
||||
|
||||
.prose-compact :not(pre) > code::before,
|
||||
.prose-compact :not(pre) > code::after {
|
||||
content: none !important;
|
||||
}
|
||||
.prose-compact :not(pre) > code::before,
|
||||
.prose-compact :not(pre) > code::after {
|
||||
content: none !important;
|
||||
}
|
||||
|
||||
.prose-compact pre {
|
||||
@apply my-3 p-4 bg-gray-100 text-gray-800 rounded-sm overflow-x-auto border border-gray-200;
|
||||
overflow-x: auto;
|
||||
max-width: 100%;
|
||||
width: 0;
|
||||
min-width: 100%;
|
||||
}
|
||||
.prose-compact pre {
|
||||
@apply my-3 p-4 bg-gray-100 text-gray-800 rounded-sm overflow-x-auto border border-gray-200;
|
||||
overflow-x: auto;
|
||||
max-width: 100%;
|
||||
width: 0;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.prose-compact pre code {
|
||||
@apply bg-transparent text-gray-800 p-0 rounded-none;
|
||||
}
|
||||
.prose-compact pre code {
|
||||
@apply bg-transparent text-gray-800 p-0 rounded-none;
|
||||
}
|
||||
|
||||
.prose-compact pre code::before,
|
||||
.prose-compact pre code::after {
|
||||
content: none !important;
|
||||
}
|
||||
.prose-compact pre code::before,
|
||||
.prose-compact pre code::after {
|
||||
content: none !important;
|
||||
}
|
||||
|
||||
.prose-compact table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
@apply my-6;
|
||||
font-size: 14px;
|
||||
line-height: 1.45;
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
.prose-compact table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
@apply my-6;
|
||||
font-size: 14px;
|
||||
line-height: 1.45;
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.prose-compact table thead,
|
||||
.prose-compact table tbody {
|
||||
display: table;
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
}
|
||||
.prose-compact table thead,
|
||||
.prose-compact table tbody {
|
||||
display: table;
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.prose-compact table tr {
|
||||
display: table-row;
|
||||
background-color: #fff;
|
||||
border-top: 1px solid #c6cbd1;
|
||||
}
|
||||
.prose-compact table tr {
|
||||
display: table-row;
|
||||
background-color: #fff;
|
||||
border-top: 1px solid #c6cbd1;
|
||||
}
|
||||
|
||||
.prose-compact table tr:nth-child(2n) {
|
||||
background-color: #f6f8fa;
|
||||
}
|
||||
.prose-compact table tr:nth-child(2n) {
|
||||
background-color: #f6f8fa;
|
||||
}
|
||||
|
||||
.prose-compact table th,
|
||||
.prose-compact table td {
|
||||
display: table-cell;
|
||||
padding: 6px 13px;
|
||||
border: 1px solid #dfe2e5;
|
||||
white-space: normal;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
min-width: 0;
|
||||
}
|
||||
.prose-compact table th,
|
||||
.prose-compact table td {
|
||||
display: table-cell;
|
||||
padding: 6px 13px;
|
||||
border: 1px solid #dfe2e5;
|
||||
white-space: normal;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.prose-compact table th {
|
||||
font-weight: 600;
|
||||
background-color: #f6f8fa;
|
||||
}
|
||||
.prose-compact table th {
|
||||
font-weight: 600;
|
||||
background-color: #f6f8fa;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ post.title }}{% endblock %}
|
||||
{% block title %}Edit: {{ post.title }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<article>
|
||||
<header class="mb-4">
|
||||
<h1 class="text-3xl md:text-4xl font-bold text-gray-900 mb-4 leading-tight">{{ post.title }}</h1>
|
||||
<h1 class="text-4xl font-bold text-gray-900 mb-4 leading-tight">{{ post.title }}</h1>
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between text-sm text-gray-600">
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="flex items-center">
|
||||
@@ -33,9 +33,70 @@
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
{% if session_username.as_deref() == Some(post.author) %}
|
||||
<div class="mt-4 sm:mt-0">
|
||||
<button onclick="document.getElementById('edit-form').classList.toggle('hidden')"
|
||||
class="inline-flex items-center px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-md transition-colors">
|
||||
<svg class="w-4 h-4 mr-1"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</header>
|
||||
<div class="prose-compact">{{ post.content | safe }}</div>
|
||||
|
||||
{% if session_username.as_deref() == Some(post.author) %}
|
||||
<div id="edit-form" class="hidden bg-gray-50 border border-gray-200 rounded-lg p-6">
|
||||
<h2 class="text-xl font-bold text-gray-900 mb-4">Edit post</h2>
|
||||
<form hx-put="/posts/{{ post.post_id }}"
|
||||
hx-target="#edit-messages"
|
||||
hx-swap="innerHTML">
|
||||
<div class="mb-4">
|
||||
<label for="title" class="block text-sm font-medium text-gray-700 mb-2">Title</label>
|
||||
<input type="text"
|
||||
id="title"
|
||||
name="title"
|
||||
value="{{ post.title }}"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="content" class="block text-sm font-medium text-gray-700 mb-2">Content (markdown)</label>
|
||||
<textarea id="content"
|
||||
name="content"
|
||||
rows="12"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 font-mono text-sm">{{ post.content }}</textarea>
|
||||
</div>
|
||||
<div class="flex items-center space-x-3">
|
||||
<button type="submit"
|
||||
class="inline-flex items-center px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-md transition-colors">
|
||||
<svg class="w-4 h-4 mr-2"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
Save changes
|
||||
</button>
|
||||
<button type="button"
|
||||
onclick="document.getElementById('edit-form').classList.add('hidden')"
|
||||
class="inline-flex items-center px-4 py-2 bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium rounded-md transition-colors">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div id="edit-messages" class="mt-6"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div id="content-display" class="prose-compact">{{ post_html | safe }}</div>
|
||||
</article>
|
||||
<div class="mt-8">{% include "posts/comments/list.html" %}</div>
|
||||
<div class="mt-8 bg-gradient-to-r from-blue-600 to-indigo-700 rounded-lg shadow-lg text-white p-8 text-center">
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<div class="bg-white rounded-lg shadow-md border border-gray-200 p-8">
|
||||
<h2 class="text-xl font-semibold text-gray-900 mb-6">Profile Information</h2>
|
||||
|
||||
<form hx-put="/users/{{ user.username }}/edit"
|
||||
<form hx-put="/users/edit"
|
||||
hx-target="#edit-messages"
|
||||
hx-swap="innerHTML"
|
||||
class="space-y-6">
|
||||
<input type="hidden" name="user_id" value="{{ user.user_id }}" />
|
||||
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
@@ -45,7 +46,7 @@
|
||||
|
||||
<button type="submit"
|
||||
class="w-full bg-blue-600 text-white hover:bg-blue-700 font-medium py-2 px-4 rounded-md transition-colors">
|
||||
Save Changes
|
||||
Save changes
|
||||
</button>
|
||||
|
||||
<div id="edit-messages"></div>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if session_username.as_deref() == Some(user.username) %}
|
||||
<a href="/users/{{ user.username }}/edit"
|
||||
<a href="/users/edit"
|
||||
class="inline-flex items-center text-sm text-blue-600 hover:text-blue-700 font-medium mb-3">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
|
||||
Reference in New Issue
Block a user