add: chats list page

This commit is contained in:
trisua 2025-08-26 00:24:12 -04:00
parent c48cf78314
commit 747a05d649
16 changed files with 576 additions and 24 deletions

View file

@ -17,6 +17,10 @@
--color-green-lowered: hsl(100, 84%, 15%); --color-green-lowered: hsl(100, 84%, 15%);
--color-red-lowered: hsl(0, 84%, 35%); --color-red-lowered: hsl(0, 84%, 35%);
--color-primary: hsl(25, 95%, 53%);
--color-primary-lowered: hsl(25, 95%, 49%);
--color-text-primary: hsl(0, 0%, 5%);
--shadow-x-offset: 0; --shadow-x-offset: 0;
--shadow-y-offset: 0.125rem; --shadow-y-offset: 0.125rem;
--shadow-size: var(--pad-1); --shadow-size: var(--pad-1);
@ -51,6 +55,10 @@
--color-green: hsl(100, 94%, 82%); --color-green: hsl(100, 94%, 82%);
--color-yellow: oklch(90.1% 0.076 70.697); --color-yellow: oklch(90.1% 0.076 70.697);
--color-purple: hsl(284, 94%, 82%); --color-purple: hsl(284, 94%, 82%);
--color-primary: hsl(25, 95%, 63%);
--color-primary-lowered: hsl(25, 95%, 59%);
--color-text-primary: hsl(0, 0%, 5%);
} }
html, html,
@ -94,7 +102,7 @@ article {
overflow: auto; overflow: auto;
} }
.tabs .tab { .tabs:not(.short) .tab {
height: 100%; height: 100%;
} }
@ -195,6 +203,10 @@ video {
padding: var(--pad-2) var(--pad-4); padding: var(--pad-2) var(--pad-4);
} }
.card.surface {
background: var(--color-surface);
}
/* button */ /* button */
.button { .button {
--h: 36px; --h: 36px;
@ -215,6 +227,7 @@ video {
text-decoration: none !important; text-decoration: none !important;
user-select: none; user-select: none;
appearance: none; appearance: none;
overflow: hidden;
} }
.button:disabled { .button:disabled {
@ -276,6 +289,15 @@ video {
} }
} }
.button.primary {
color: var(--color-text-primary) !important;
background: var(--color-primary);
}
.button.primary:hover {
background: var(--color-primary-lowered) !important;
}
/* dropdown */ /* dropdown */
.dropdown { .dropdown {
position: relative; position: relative;
@ -468,6 +490,12 @@ svg.icon {
fill: currentColor; fill: currentColor;
} }
.big_icon svg.icon {
width: 18px;
height: 18px;
position: absolute;
}
button svg { button svg {
pointer-events: none; pointer-events: none;
} }
@ -621,6 +649,10 @@ span {
align-items: flex-end; align-items: flex-end;
} }
.gap_ch {
gap: 1ch;
}
/* table */ /* table */
table { table {
width: 100%; width: 100%;
@ -675,7 +707,7 @@ dialog {
border: 0; border: 0;
} }
dialog.inner { dialog .inner {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--pad-2); gap: var(--pad-2);

View file

@ -0,0 +1,26 @@
(text "{% extends \"root.lisp\" %} {% block head %}")
(title
(text "{{ components::chat_name(chat=chat, members=members) }} - {{ name }}"))
(text "{% endblock %} {% block body %}")
(div
("class" "flex w_full gap_2 justify_between items_center")
(div
("class" "tabs short bar flex")
(a
("class" "button tab camo")
("href" "/chats")
(text "chats"))
(a
("class" "button tab")
("href" "/chats/{{ chat.id }}")
(text "{{ components::chat_name(chat=chat, members=members, advanced=true, avatar_size=\"18px\") }}"))))
(div
("class" "card flex flex_col gap_2")
("style" "flex: 1 0 auto")
(text "{% if messages|length == 0 -%}")
(i ("class" "flex gap_ch items_center fade") (text "{{ icon \"star\" }} This is the start of the chat!"))
(text "{%- endif %}"))
(script
(text ""))
(text "{% endblock %}")

View file

@ -0,0 +1,217 @@
(text "{% extends \"root.lisp\" %} {% block head %}")
(title
(text "My chats - {{ name }}"))
(text "{% endblock %} {% block body %}")
(div
("class" "flex w_full gap_2 justify_between items_center")
(div
("class" "tabs short bar")
(a
("class" "button tab")
("href" "/chats")
(text "chats")))
(button
("class" "button")
("title" "Create chat")
("onclick" "document.getElementById('create_dialog').showModal()")
(text "{{ icon \"plus\" }}")))
(div
("class" "card flex flex_col gap_2")
(text "{% for chat in chats -%}")
(div
("class" "card surface w_full flex justify_between items_center gap_2")
(a
("class" "flex gap_ch items_center")
("href" "/chats/{{ chat[0].id }}")
(text "{{ components::chat_name(chat=chat[0], members=chat[1], advanced=true) }}"))
(div
("class" "dropdown")
(button
("onclick" "open_dropdown(event)")
("exclude" "dropdown")
("class" "button")
(text "{{ icon \"ellipsis\" }}"))
(div
("class" "inner")
(a
("class" "button")
("href" "/chats/{{ chat[0].id }}")
("target" "_blank")
(text "pop open"))
(text "{% if chat[0].style != \"Direct\" -%}")
; group chat only
(button
("class" "button")
("onclick" "rename_gc('{{ chat[0].id }}')")
(text "rename"))
(text "{%- endif %}")
(button
("class" "button red")
("onclick" "leave_chat('{{ chat[0].id }}')")
(text "leave")))))
(text "{%- endfor %}")
(text "{% if chats|length == 0 -%}")
(i ("class" "flex gap_ch items_center fade") (text "{{ icon \"smile\" }} No results, yet!"))
(text "{%- endif %}")
; pagination
(div
("class" "flex w_full items_center gap_2 justify_between")
(text "{% if page > 0 -%}")
(a
("href" "?page={{ page - 1 }}")
("class" "button surface")
(text "{{ icon \"arrow-left\" }} Back"))
(text "{%- else -%}")
(div null?)
(text "{%- endif %}")
(text "{% if chats|length > 0 -%}")
(a
("href" "?page={{ page + 1 }}")
("class" "button surface")
(text "Next {{ icon \"arrow-right\" }}"))
(text "{%- endif %}")))
(dialog
("id" "create_dialog")
(div
("class" "inner")
(h2
("class" "text_center w_full")
(text "Create chat"))
(ul ("id" "members_list"))
(form
("class" "flex flex_row gap_2 w_full")
("onsubmit" "add_member(event)")
(input
("type" "text")
("list" "users_search")
("name" "username")
("id" "username")
("placeholder" "username")
("oninput" "search_users(event)"))
(button
("class" "button")
(text "Add")))
(hr ("class" "margin"))
(div
("class" "flex gap_2 justify_between")
(button
("onclick" "document.getElementById('create_dialog').close()")
("class" "button red")
(text "Cancel"))
(button
("class" "button green")
("onclick" "create_chat_from_form()")
(text "Create")))))
(datalist ("id" "users_search"))
(script
(text "globalThis.CHAT_MEMBERS = [];
function render_members() {
document.getElementById('members_list').innerHTML = \"\";
for (const member of CHAT_MEMBERS) {
document.getElementById('members_list').innerHTML += `<li>${member} (<a href=\"javascript:remove_member('${member}')\" class=\"red\">remove</a>)</li>`;
}
}
function add_member(e) {
e.preventDefault();
const member = e.target.username.value;
if (CHAT_MEMBERS.includes(member)) {
return;
}
CHAT_MEMBERS.push(member);
render_members();
e.target.reset();
}
function remove_member(member) {
CHAT_MEMBERS.splice(CHAT_MEMBERS.indexOf(member), 1);
render_members();
}
function create_chat_from_form(e) {
document.getElementById('create_dialog').close();
fetch(\"/api/v1/chats\", {
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify({
style: CHAT_MEMBERS.length > 1 ? { Group: { name: \"Untitled\" } } : \"Direct\",
members: CHAT_MEMBERS,
}),
})
.then((res) => res.json())
.then((res) => {
show_message(res.message, res.ok);
if (res.ok) {
window.location.href = `/chats/${res.payload}`;
e.target.reset();
}
});
}
let search_users_timeout;
function search_users(e) {
if (search_users_timeout) {
clearTimeout(search_users_timeout);
}
if (e.target.value.trim().length === 0) {
return;
}
search_users_timeout = setTimeout(() => {
fetch(\"/api/v1/auth/users/search\", {
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify({
prefix: e.target.value.trim(),
}),
})
.then((res) => res.json())
.then((res) => {
if (res.ok) {
document.getElementById(\"users_search\").innerHTML = \"\";
for (const username of res.payload) {
document.getElementById(\"users_search\").innerHTML += `<option value=\"${username}\">${username}</option>`;
}
} else {
show_message(res.message, res.ok);
}
});
}, 1000);
}
function leave_chat(id) {
if (!confirm(\"Are you sure you would like to do this?\")) {
return;
}
fetch(`/api/v1/chats/${id}/leave`, {
method: \"POST\",
})
.then((res) => res.json())
.then((res) => {
show_message(res.message, res.ok);
});
}"))
(text "{% endblock %}")

View file

@ -7,3 +7,36 @@
("loading" "lazy") ("loading" "lazy")
("style" "--size: {{ size }}")) ("style" "--size: {{ size }}"))
(text "{%- endmacro %}") (text "{%- endmacro %}")
(text "{% macro username(user) -%}")
(b
(text "{% if user.settings.display_name|length > 0 -%}")
(text "{{ user.settings.display_name }}")
(text "{%- else -%}")
(text "{{ user.username }}")
(text "{%- endif %}"))
(text "{%- endmacro %}")
(text "{% macro chat_name(chat, members, advanced=false, avatar_size=\"24px\") -%}")
(text "{% if advanced -%}")
; advanced
(text "{% if chat.style == \"Direct\" -%} {% for member in members -%} {% if member.id != user.id -%}")
; direct message; user that ISN'T the current user
(text "{{ components::avatar(id=member.id, size=avatar_size) }}")
(text "{{ components::username(user=member) }}")
(text "{%- endif %} {%- endfor %} {%- else -%}")
; group chat
(text "{% for member in members -%} {{ components::avatar(id=member.id, size=avatar_size) }} {%- endfor %}")
(b (text "{{ chat.style.Group.name }}"))
(text "{%- endif %}")
(text "{%- else -%}")
; NOT advanced
(text "{% if chat.style == \"Direct\" -%} {% for member in members -%} {% if member.id != user.id -%}")
; direct message; user that ISN'T the current user
(text "{{ user.username }}")
(text "{%- endif %} {%- endfor %} {%- else -%}")
; group chat
(text "{{ chat.style.Group.name }}")
(text "{%- endif %}")
(text "{%- endif %}")
(text "{%- endmacro %}")

View file

@ -10,8 +10,6 @@
(link ("rel" "stylesheet") ("href" "https://repodelivery.tetratto.com/tetratto/crates/app/src/public/css/utility.css")) (link ("rel" "stylesheet") ("href" "https://repodelivery.tetratto.com/tetratto/crates/app/src/public/css/utility.css"))
(link ("rel" "stylesheet") ("href" "/public/style.css?v={{ build_code }}")) (link ("rel" "stylesheet") ("href" "/public/style.css?v={{ build_code }}"))
(style (text ":root { --color-primary: {{ theme_color }}; }"))
(meta ("name" "theme-color") ("content" "{{ theme_color }}")) (meta ("name" "theme-color") ("content" "{{ theme_color }}"))
(meta ("property" "og:type") ("content" "website")) (meta ("property" "og:type") ("content" "website"))
(meta ("property" "og:site_name") ("content" "{{ name }}")) (meta ("property" "og:site_name") ("content" "{{ name }}"))
@ -28,7 +26,7 @@
(body (body
; nav ; nav
(nav (nav
("class" "flex w_full justify_between gap_2 sticky") ("class" "flex w_full justify_between gap_2")
(div (div
("class" "flex side") ("class" "flex side")
(div (div
@ -76,8 +74,7 @@
("onclick" "user_logout()") ("onclick" "user_logout()")
(text "logout")) (text "logout"))
(text "{%- endif %}") (text "{%- endif %}")
(text "{% block dropdown %}{% endblock %}"))) (text "{% block dropdown %}{% endblock %}"))))
(a ("class" "button camo") ("href" "/") (b (text "{{ name }}"))))
(div (div
("class" "side flex") ("class" "side flex")

View file

@ -1,9 +1,9 @@
use super::DataManager; use super::DataManager;
use crate::model::{Chat, ChatStyle}; use crate::model::{Chat, ChatStyle};
use oiseau::{PostgresRow, cache::Cache, execute, get, params}; use oiseau::{PostgresRow, cache::Cache, execute, get, params, query_rows};
use tetratto_core::{ use tetratto_core::{
auto_method, auto_method,
model::{Error, Result}, model::{Error, Result, auth::User},
}; };
impl DataManager { impl DataManager {
@ -21,6 +21,59 @@ impl DataManager {
auto_method!(get_chat_by_id(usize as i64)@get_chat_from_row -> "SELECT * FROM t_chats WHERE id = $1" --name="chat" --returns=Chat --cache-key-tmpl="twny.chat:{}"); auto_method!(get_chat_by_id(usize as i64)@get_chat_from_row -> "SELECT * FROM t_chats WHERE id = $1" --name="chat" --returns=Chat --cache-key-tmpl="twny.chat:{}");
pub async fn fill_chat(&self, chat: Chat) -> (Chat, Vec<User>) {
let mut members = Vec::new();
for x in &chat.members {
members.push(match self.2.get_user_by_id(*x).await {
Ok(x) => x,
Err(_) => User::deleted(),
});
}
(chat, members)
}
pub async fn fill_chats(&self, chats: Vec<Chat>) -> Vec<(Chat, Vec<User>)> {
let mut out = Vec::new();
for x in chats {
out.push(self.fill_chat(x).await);
}
out
}
/// Get all chats the given member is participating in.
pub async fn get_chats_by_member(
&self,
id: usize,
batch: usize,
page: usize,
) -> Result<Vec<Chat>> {
let conn = match self.0.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = query_rows!(
&conn,
"SELECT * FROM t_chats WHERE members LIKE $1 ORDER BY last_message_created LIMIT $2 OFFSET $3",
params![
&format!("%{id}%"),
&(batch as i64),
&((page * batch) as i64)
],
|x| { Self::get_chat_from_row(x) }
);
if let Err(e) = res {
return Err(Error::DatabaseError(e.to_string()));
}
Ok(res.unwrap())
}
/// Create a new chat in the database. /// Create a new chat in the database.
/// ///
/// # Arguments /// # Arguments

View file

@ -3,7 +3,7 @@ use crate::model::{Message, SocketMessage, SocketMethod};
use oiseau::{ use oiseau::{
PostgresRow, PostgresRow,
cache::{Cache, redis::Commands}, cache::{Cache, redis::Commands},
execute, get, params, execute, get, params, query_rows,
}; };
use tetratto_core::{ use tetratto_core::{
auto_method, auto_method,
@ -27,6 +27,32 @@ impl DataManager {
auto_method!(get_message_by_id(usize as i64)@get_message_from_row -> "SELECT * FROM t_messages WHERE id = $1" --name="message" --returns=Message --cache-key-tmpl="twny.message:{}"); auto_method!(get_message_by_id(usize as i64)@get_message_from_row -> "SELECT * FROM t_messages WHERE id = $1" --name="message" --returns=Message --cache-key-tmpl="twny.message:{}");
/// Get messages by their chat ID.
pub async fn get_messages_by_chat(
&self,
id: usize,
batch: usize,
page: usize,
) -> Result<Vec<Message>> {
let conn = match self.0.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = query_rows!(
&conn,
"SELECT * FROM t_messages WHERE chat = $1 LIMIT $2 OFFSET $3",
params![&(id as i64), &(batch as i64), &((page * batch) as i64)],
|x| { Self::get_message_from_row(x) }
);
if let Err(e) = res {
return Err(Error::DatabaseError(e.to_string()));
}
Ok(res.unwrap())
}
/// Create a new message in the database. /// Create a new message in the database.
/// ///
/// # Arguments /// # Arguments

View file

@ -1,6 +1,7 @@
mod chats; mod chats;
mod messages; mod messages;
mod sql; mod sql;
mod users;
use crate::config::Config; use crate::config::Config;
use buckets_core::{Config as BucketsConfig, DataManager as BucketsManager}; use buckets_core::{Config as BucketsConfig, DataManager as BucketsManager};

View file

@ -3,7 +3,7 @@ CREATE TABLE IF NOT EXISTS t_messages (
created BIGINT NOT NULL, created BIGINT NOT NULL,
edited BIGINT NOT NULL, edited BIGINT NOT NULL,
owner BIGINT NOT NULL, owner BIGINT NOT NULL,
chats BIGINT NOT NULL, chat BIGINT NOT NULL,
content TEXT NOT NULL, content TEXT NOT NULL,
uploads TEXT NOT NULL uploads TEXT NOT NULL
); );

26
src/database/users.rs Normal file
View file

@ -0,0 +1,26 @@
use super::DataManager;
use oiseau::{get, params, query_rows};
use tetratto_core::model::{Error, Result};
impl DataManager {
/// Search all users.
pub async fn search_users(&self, query: &str, batch: usize) -> Result<Vec<String>> {
let conn = match self.0.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = query_rows!(
&conn,
"SELECT username, id FROM users WHERE username LIKE $1 LIMIT $2",
params![&format!("{query}%"), &(batch as i64),],
|x| { get!(x->0(String)) }
);
if let Err(e) = res {
return Err(Error::DatabaseError(e.to_string()));
}
Ok(res.unwrap())
}
}

View file

@ -189,3 +189,28 @@ pub async fn check_totp_request(
payload: Some(!user.totp.is_empty()), payload: Some(!user.totp.is_empty()),
}) })
} }
#[derive(Deserialize)]
pub struct SearchUsers {
pub prefix: String,
}
pub async fn search_users_request(
jar: CookieJar,
Extension(data): Extension<State>,
Json(props): Json<SearchUsers>,
) -> impl IntoResponse {
let data = &(data.read().await).0;
if get_user_from_token!(jar, data.2).is_none() {
return Json(Error::NotAllowed.into());
}
match data.search_users(&props.prefix, 4).await {
Ok(x) => Json(ApiReturn {
ok: true,
message: "Success".to_string(),
payload: Some(x),
}),
Err(e) => Json(e.into()),
}
}

View file

@ -22,7 +22,7 @@ use tetratto_core::model::{ApiReturn, Error, auth::User};
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct CreateChat { pub struct CreateChat {
pub style: ChatStyle, pub style: ChatStyle,
pub members: Vec<usize>, pub members: Vec<String>,
} }
pub async fn create_request( pub async fn create_request(
@ -37,13 +37,23 @@ pub async fn create_request(
}; };
if req.members.len() > 2 && req.style == ChatStyle::Direct { if req.members.len() > 2 && req.style == ChatStyle::Direct {
return Json(Error::DataTooLong("members".to_string()).into()); return Json(Error::DataTooLong("members list".to_string()).into());
} else if req.members.len() < 1 {
return Json(Error::DataTooShort("members list".to_string()).into());
} }
match data match data
.create_chat(Chat::new(req.style, { .create_chat(Chat::new(req.style, {
let mut x = req.members; let mut x = Vec::new();
x.push(user.id); x.push(user.id);
for y in req.members {
x.push(match data.2.get_user_by_username(&y).await {
Ok(x) => x.id,
Err(e) => return Json(e.into()),
})
}
x x
})) }))
.await .await

View file

@ -14,6 +14,7 @@ pub fn routes() -> Router {
"/auth/user/{username}/check_totp", "/auth/user/{username}/check_totp",
get(auth::check_totp_request), get(auth::check_totp_request),
) )
.route("/auth/users/search", post(auth::search_users_request))
// chats // chats
.route("/chats", post(chats::create_request)) .route("/chats", post(chats::create_request))
.route("/chats/{id}/leave", post(chats::leave_request)) .route("/chats/{id}/leave", post(chats::leave_request))

87
src/routes/pages/chats.rs Normal file
View file

@ -0,0 +1,87 @@
use crate::{
State, get_user_from_token,
routes::{
default_context,
pages::{PaginatedQuery, misc::render_error},
},
};
use axum::{
Extension,
extract::{Path, Query},
response::{Html, IntoResponse},
};
use axum_extra::extract::CookieJar;
use tetratto_core::model::Error;
pub async fn list_request(
jar: CookieJar,
Extension(data): Extension<State>,
Query(props): Query<PaginatedQuery>,
) -> impl IntoResponse {
let (ref data, ref tera, ref build_code) = *data.read().await;
let user = match get_user_from_token!(jar, data.2) {
Some(x) => x,
None => {
return Err(render_error(Error::NotAllowed, tera, data.0.0.clone(), None).await);
}
};
let chats = match data.get_chats_by_member(user.id, 12, props.page).await {
Ok(x) => data.fill_chats(x).await,
Err(e) => {
return Err(render_error(e, tera, data.0.0.clone(), Some(user)).await);
}
};
let mut ctx = default_context(&data.0.0, &build_code, &Some(user));
ctx.insert("chats", &chats);
ctx.insert("page", &props.page);
Ok(Html(tera.render("chats.lisp", &ctx).unwrap()))
}
pub async fn chat_request(
jar: CookieJar,
Extension(data): Extension<State>,
Path(id): Path<usize>,
Query(props): Query<PaginatedQuery>,
) -> impl IntoResponse {
let (ref data, ref tera, ref build_code) = *data.read().await;
let user = match get_user_from_token!(jar, data.2) {
Some(x) => x,
None => {
return Err(render_error(Error::NotAllowed, tera, data.0.0.clone(), None).await);
}
};
let (chat, members) = match data.get_chat_by_id(id).await {
Ok(x) => {
if !x.members.contains(&user.id) {
return Err(
render_error(Error::NotAllowed, tera, data.0.0.clone(), Some(user)).await,
);
}
data.fill_chat(x).await
}
Err(e) => {
return Err(render_error(e, tera, data.0.0.clone(), Some(user)).await);
}
};
let messages = match data.get_messages_by_chat(id, 12, props.page).await {
Ok(x) => x,
Err(e) => {
return Err(render_error(e, tera, data.0.0.clone(), Some(user)).await);
}
};
let mut ctx = default_context(&data.0.0, &build_code, &Some(user));
ctx.insert("chat", &chat);
ctx.insert("members", &members);
ctx.insert("messages", &messages);
Ok(Html(tera.render("chat.lisp", &ctx).unwrap()))
}

View file

@ -1,24 +1,30 @@
use crate::{State, get_user_from_token, routes::default_context}; use crate::{State, config::Config, get_user_from_token, routes::default_context};
use axum::{ use axum::{
Extension, Extension,
response::{Html, IntoResponse}, response::{Html, IntoResponse},
}; };
use axum_extra::extract::CookieJar; use axum_extra::extract::CookieJar;
use tetratto_core::model::Error; use tera::Tera;
use tetratto_core::model::{Error, auth::User};
pub async fn render_error(
e: Error,
tera: &Tera,
config: Config,
user: Option<User>,
) -> impl IntoResponse + use<> {
let mut ctx = default_context(&config, "", &user);
ctx.insert("error", &e.to_string());
return Html(tera.render("error.lisp", &ctx).unwrap());
}
pub async fn not_found_request( pub async fn not_found_request(
jar: CookieJar, jar: CookieJar,
Extension(data): Extension<State>, Extension(data): Extension<State>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let (ref data, ref tera, ref build_code) = *data.read().await; let (ref data, ref tera, _) = *data.read().await;
let user = get_user_from_token!(jar, data.2); let user = get_user_from_token!(jar, data.2);
render_error(Error::NotAllowed, tera, data.0.0.clone(), user).await
let mut ctx = default_context(&data.0.0, &build_code, &user);
ctx.insert(
"error",
&Error::GeneralNotFound("page".to_string()).to_string(),
);
return Html(tera.render("error.lisp", &ctx).unwrap());
} }
pub async fn index_request(jar: CookieJar, Extension(data): Extension<State>) -> impl IntoResponse { pub async fn index_request(jar: CookieJar, Extension(data): Extension<State>) -> impl IntoResponse {

View file

@ -1,9 +1,21 @@
pub mod chats;
pub mod misc; pub mod misc;
use axum::routing::{Router, get}; use axum::routing::{Router, get};
use serde::Deserialize;
pub fn routes() -> Router { pub fn routes() -> Router {
Router::new() Router::new()
.route("/", get(misc::index_request)) .route("/", get(misc::index_request))
// auth
.route("/login", get(misc::login_request)) .route("/login", get(misc::login_request))
// chats
.route("/chats", get(chats::list_request))
.route("/chats/{id}", get(chats::chat_request))
}
#[derive(Deserialize)]
pub struct PaginatedQuery {
#[serde(default)]
pub page: usize,
} }