start
This commit is contained in:
parent
8afdd208c6
commit
8e82ec9523
23 changed files with 3345 additions and 0 deletions
2199
Cargo.lock
generated
Normal file
2199
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
3
Cargo.toml
Normal file
3
Cargo.toml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[workspace]
|
||||
members = ["froxy-scraper", "froxy-templates"]
|
||||
resolver = "3"
|
||||
15
froxy-scraper/Cargo.toml
Normal file
15
froxy-scraper/Cargo.toml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
[package]
|
||||
name = "froxy-scrapper"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
constcat = "0.6.1"
|
||||
reqwest = "0.13.1"
|
||||
scraper = "0.25.0"
|
||||
secrecy = "0.10.3"
|
||||
smol_str = "0.3.5"
|
||||
url = "2.5.8"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1", features = ["rt", "macros"] }
|
||||
37
froxy-scraper/examples/get_frontpage.rs
Normal file
37
froxy-scraper/examples/get_frontpage.rs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
use froxy_scrapper::{state::State, types::ClusterLocation};
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() {
|
||||
let session = std::env::var("FROXY_COOKIE")
|
||||
.expect("provide `FROXY_COOKIE` with the session cookie from you friends42 instance");
|
||||
let state = State::new(session);
|
||||
|
||||
let res = state.get("/", Some("cluster=f4")).await.unwrap();
|
||||
|
||||
let text = res.text().await.unwrap();
|
||||
|
||||
let location = froxy_scrapper::parsing::get_cluster_location("F4", &text);
|
||||
|
||||
for l in &location.locations {
|
||||
match &l.data {
|
||||
froxy_scrapper::types::ClusterLocationData::Empty => {}
|
||||
froxy_scrapper::types::ClusterLocationData::User {
|
||||
login,
|
||||
image,
|
||||
relation,
|
||||
} => {
|
||||
eprintln!(
|
||||
"[{:<10}] {login:<10} {relation:?} {}",
|
||||
l.location,
|
||||
image
|
||||
.as_ref()
|
||||
.map(|i| i.to_string())
|
||||
.unwrap_or_else(|| "NO IMAGE".to_string())
|
||||
)
|
||||
}
|
||||
froxy_scrapper::types::ClusterLocationData::Normal { status } => {
|
||||
//eprintln!("[{:<10}] {status:?}", l.location)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
5
froxy-scraper/src/lib.rs
Normal file
5
froxy-scraper/src/lib.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
#![allow(dead_code)]
|
||||
|
||||
pub mod types;
|
||||
pub mod state;
|
||||
pub mod parsing;
|
||||
121
froxy-scraper/src/parsing.rs
Normal file
121
froxy-scraper/src/parsing.rs
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
use scraper::Selector;
|
||||
use url::Url;
|
||||
|
||||
use crate::types::{ClusterLocation, ClusterLocationData, Relation};
|
||||
|
||||
macro_rules! sel {
|
||||
($name:ident, $value:literal) => {
|
||||
static $name: std::sync::LazyLock<Selector> =
|
||||
std::sync::LazyLock::new(|| Selector::parse($value).unwrap());
|
||||
};
|
||||
}
|
||||
|
||||
sel!(
|
||||
LOCATION_SEL,
|
||||
"div.container-fluid.mt-2.scroll.p-0 > table.grid.text-center > tbody > tr > td"
|
||||
);
|
||||
sel!(IMAGE_SEL, "img");
|
||||
|
||||
pub fn get_cluster_location(
|
||||
cluster_name: impl AsRef<str>,
|
||||
page: impl AsRef<str>,
|
||||
) -> crate::types::CluserInformation {
|
||||
let page = page.as_ref();
|
||||
|
||||
let doc = scraper::Html::parse_document(page);
|
||||
let tds = doc.select(&LOCATION_SEL);
|
||||
|
||||
let mut out = crate::types::CluserInformation {
|
||||
cluster_name: cluster_name.as_ref().into(),
|
||||
locations: Vec::with_capacity(128),
|
||||
};
|
||||
|
||||
for td in tds {
|
||||
let pos_login = td.attr("data-pos").and_then(|pos| {
|
||||
td.attr("data-login")
|
||||
.map(|login| (pos.trim(), login.trim()))
|
||||
});
|
||||
let is_dead = td
|
||||
.value()
|
||||
.has_class("dead", scraper::CaseSensitivity::AsciiCaseInsensitive);
|
||||
let is_warn = td
|
||||
.value()
|
||||
.has_class("attention", scraper::CaseSensitivity::AsciiCaseInsensitive);
|
||||
|
||||
match (pos_login, is_dead, is_warn) {
|
||||
(Some((pos, login)), d, w) if login.is_empty() && (d || w) => {
|
||||
out.locations.push(match (d, w) {
|
||||
(true, _) => ClusterLocation {
|
||||
location: pos.into(),
|
||||
data: ClusterLocationData::Normal {
|
||||
status: crate::types::ClusterLocationStatus::Dead,
|
||||
},
|
||||
},
|
||||
(false, true) => ClusterLocation {
|
||||
location: pos.into(),
|
||||
data: ClusterLocationData::Normal {
|
||||
status: crate::types::ClusterLocationStatus::Damaged,
|
||||
},
|
||||
},
|
||||
// this should never happen
|
||||
_ => ClusterLocation {
|
||||
location: pos.into(),
|
||||
data: ClusterLocationData::Empty,
|
||||
},
|
||||
})
|
||||
}
|
||||
(Some((pos, login)), _, _) if !login.trim().is_empty() => {
|
||||
let image = td
|
||||
.select(&IMAGE_SEL)
|
||||
.next()
|
||||
.and_then(|i| i.attr("src"))
|
||||
.and_then(|s| Url::parse(s).ok());
|
||||
let is_me = td
|
||||
.value()
|
||||
.has_class("me", scraper::CaseSensitivity::AsciiCaseInsensitive);
|
||||
let is_focus = td
|
||||
.value()
|
||||
.has_class("focus", scraper::CaseSensitivity::AsciiCaseInsensitive);
|
||||
let is_friend = td
|
||||
.value()
|
||||
.has_class("friend", scraper::CaseSensitivity::AsciiCaseInsensitive);
|
||||
let is_close_friend = td.value().has_class(
|
||||
"close_friend",
|
||||
scraper::CaseSensitivity::AsciiCaseInsensitive,
|
||||
);
|
||||
let is_pooled = td
|
||||
.value()
|
||||
.has_class("pooled", scraper::CaseSensitivity::AsciiCaseInsensitive);
|
||||
|
||||
let mut relation = Relation::None;
|
||||
if is_me {
|
||||
relation = Relation::Me;
|
||||
}
|
||||
if is_friend {
|
||||
relation = Relation::Friend;
|
||||
}
|
||||
if is_close_friend {
|
||||
relation = Relation::CloseFriend;
|
||||
}
|
||||
if is_pooled {
|
||||
relation = Relation::Pooled;
|
||||
}
|
||||
if is_focus {
|
||||
relation = Relation::Focus;
|
||||
}
|
||||
|
||||
out.locations.push(ClusterLocation {
|
||||
location: pos.into(),
|
||||
data: ClusterLocationData::User {
|
||||
login: login.into(),
|
||||
relation,
|
||||
image,
|
||||
},
|
||||
})
|
||||
}
|
||||
(_, _, _) => {}
|
||||
}
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
138
froxy-scraper/src/state.rs
Normal file
138
froxy-scraper/src/state.rs
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use reqwest::{Method, Response};
|
||||
use secrecy::{ExposeSecret, SecretString};
|
||||
use url::UrlQuery;
|
||||
|
||||
macro_rules! const_from_env {
|
||||
($const:ident, $env:literal) => {
|
||||
const $const: Option<&'static str> = option_env!($env);
|
||||
};
|
||||
($const:ident, $env:literal, $default:expr) => {
|
||||
const $const: &'static str = match option_env!($env) {
|
||||
Some(s) => s,
|
||||
None => $default,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const_from_env!(USER_AGENT_APP, "FROXY_UA_APP_NAME", "Froxy");
|
||||
const_from_env!(USER_AGENT_VERSION, "FROXY_UA_APP_VERSION", {
|
||||
const_from_env!(CARGO_PKG_VERSION, "CARGO_PKG_VERSION", "0.1-alpha");
|
||||
CARGO_PKG_VERSION
|
||||
});
|
||||
const_from_env!(
|
||||
USER_AGENT_DESC,
|
||||
"FROXY_UA_DESC",
|
||||
"(by maiboyer; for maiboyer)"
|
||||
);
|
||||
|
||||
pub const DEFAULT_USER_AGENT: &str =
|
||||
constcat::concat!(USER_AGENT_APP, "/", USER_AGENT_VERSION, USER_AGENT_DESC);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct State {
|
||||
client: reqwest::Client,
|
||||
base_url: url::Url,
|
||||
secret_cookie: secrecy::SecretString,
|
||||
user_agent: std::borrow::Cow<'static, str>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn new(secret_cookie: impl AsRef<str>) -> Self {
|
||||
let user_agent = DEFAULT_USER_AGENT;
|
||||
let base_url = "https://friends.42paris.fr";
|
||||
|
||||
Self::with_info(
|
||||
secret_cookie,
|
||||
user_agent.into(),
|
||||
url::Url::parse(base_url).expect("default base url not valid"),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn with_info(
|
||||
secret_cookie: impl AsRef<str>,
|
||||
user_agent: std::borrow::Cow<'static, str>,
|
||||
base_url: url::Url,
|
||||
) -> Self {
|
||||
let client = reqwest::Client::builder()
|
||||
.user_agent(&*user_agent)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
Self {
|
||||
client,
|
||||
base_url,
|
||||
secret_cookie: secret_cookie.as_ref().into(),
|
||||
user_agent,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn client(&self) -> &reqwest::Client {
|
||||
&self.client
|
||||
}
|
||||
|
||||
pub fn base_url(&self) -> &url::Url {
|
||||
&self.base_url
|
||||
}
|
||||
|
||||
pub fn cookie(&self) -> &SecretString {
|
||||
&self.secret_cookie
|
||||
}
|
||||
|
||||
pub fn user_agent(&self) -> &str {
|
||||
&self.user_agent
|
||||
}
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub async fn request(
|
||||
&self,
|
||||
method: Method,
|
||||
url: impl AsRef<str>,
|
||||
qs: Option<&str>,
|
||||
body: Option<Vec<u8>>,
|
||||
) -> Result<Response, reqwest::Error> {
|
||||
let mut u = self.base_url.clone();
|
||||
u.set_path(url.as_ref());
|
||||
u.set_query(qs);
|
||||
let builder = self
|
||||
.client
|
||||
.request(method, u)
|
||||
.header("Cookie", self.secret_cookie.expose_secret());
|
||||
|
||||
let builder = if let Some(body) = body {
|
||||
builder.body(body)
|
||||
} else {
|
||||
builder
|
||||
};
|
||||
|
||||
builder.send().await
|
||||
}
|
||||
|
||||
pub async fn get(
|
||||
&self,
|
||||
url: impl AsRef<str>,
|
||||
qs: Option<&str>,
|
||||
) -> Result<Response, reqwest::Error> {
|
||||
self.request(Method::GET, url.as_ref(), qs, None).await
|
||||
}
|
||||
|
||||
pub async fn post(
|
||||
&self,
|
||||
url: impl AsRef<str>,
|
||||
qs: Option<&str>,
|
||||
) -> Result<Response, reqwest::Error> {
|
||||
self.request(Method::POST, url.as_ref(), qs, None).await
|
||||
}
|
||||
|
||||
pub async fn post_with_body(
|
||||
&self,
|
||||
url: impl AsRef<str>,
|
||||
qs: Option<&str>,
|
||||
body: Vec<u8>,
|
||||
) -> Result<Response, reqwest::Error> {
|
||||
self.request(Method::POST, url.as_ref(), qs, Some(body))
|
||||
.await
|
||||
}
|
||||
}
|
||||
48
froxy-scraper/src/types.rs
Normal file
48
froxy-scraper/src/types.rs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone, Default, Eq, PartialEq)]
|
||||
pub enum ClusterLocationStatus {
|
||||
#[doc(alias = "red")]
|
||||
Dead,
|
||||
#[doc(alias = "orange")]
|
||||
Damaged,
|
||||
#[doc(alias = "good")]
|
||||
#[default]
|
||||
Ok,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub enum Relation {
|
||||
CloseFriend,
|
||||
Friend,
|
||||
Pooled,
|
||||
Me,
|
||||
Focus,
|
||||
#[default]
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub enum ClusterLocationData {
|
||||
User {
|
||||
login: smol_str::SmolStr,
|
||||
image: Option<url::Url>,
|
||||
relation: Relation,
|
||||
},
|
||||
Normal {
|
||||
status: ClusterLocationStatus,
|
||||
},
|
||||
#[default]
|
||||
Empty,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ClusterLocation {
|
||||
pub location: smol_str::SmolStr,
|
||||
pub data: ClusterLocationData,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CluserInformation {
|
||||
pub cluster_name: smol_str::SmolStr,
|
||||
pub locations: Vec<ClusterLocation>,
|
||||
}
|
||||
1
froxy-templates/.gitignore
vendored
Normal file
1
froxy-templates/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/target
|
||||
9
froxy-templates/Cargo.toml
Normal file
9
froxy-templates/Cargo.toml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
[package]
|
||||
name = "froxy-templates"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
minijinja = "2.15.1"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
smol_str = { version = "0.3.5", features = ["serde"] }
|
||||
20
froxy-templates/examples/friend.rs
Normal file
20
froxy-templates/examples/friend.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
use froxy_templates::friends::{self, Friend};
|
||||
|
||||
fn main() {
|
||||
let mut env = minijinja::Environment::new();
|
||||
friends::add_to_context(&mut env);
|
||||
let data = friends::render(
|
||||
&env,
|
||||
friends::FriendsData {
|
||||
friends: std::iter::from_fn(|| Some(Friend {
|
||||
name: "maiboyer".into(),
|
||||
image: "https://friends.42paris.fr/proxy/resize/512/03c61af252becbca11aac5ff49a2e61c/maiboyer.jpg".into(),
|
||||
position: Some("f1r1s1".into()),
|
||||
last_active: None,
|
||||
})).take(10).collect(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
println!("{data}");
|
||||
}
|
||||
30
froxy-templates/src/friends.rs
Normal file
30
froxy-templates/src/friends.rs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
pub use crate::templates::FRIENDS;
|
||||
|
||||
pub fn add_to_context(env: &mut minijinja::Environment) {
|
||||
if env.get_template("template.html").is_err() {
|
||||
crate::meta_template::add_to_context(env);
|
||||
}
|
||||
if env.get_template("open_modal.html").is_err() {
|
||||
crate::open_modal::add_to_context(env);
|
||||
}
|
||||
|
||||
env.add_template("friends.html", FRIENDS).unwrap();
|
||||
}
|
||||
|
||||
pub fn render(env: &minijinja::Environment, data: FriendsData) -> Result<String, minijinja::Error> {
|
||||
let template = env.get_template("friends.html")?;
|
||||
template.render(data)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Friend {
|
||||
pub name: smol_str::SmolStr,
|
||||
pub image: String,
|
||||
pub position: Option<smol_str::SmolStr>,
|
||||
pub last_active: Option<smol_str::SmolStr>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct FriendsData {
|
||||
pub friends: Vec<Friend>,
|
||||
}
|
||||
11
froxy-templates/src/index.rs
Normal file
11
froxy-templates/src/index.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
pub use crate::templates::INDEX;
|
||||
|
||||
pub fn add_to_context(env: &mut minijinja::Environment) {
|
||||
if env.get_template("template.html").is_err() {
|
||||
crate::meta_template::add_to_context(env);
|
||||
}
|
||||
if env.get_template("open_modal.html").is_err() {
|
||||
crate::open_modal::add_to_context(env);
|
||||
}
|
||||
env.add_template("index.html", INDEX).unwrap();
|
||||
}
|
||||
9
froxy-templates/src/lib.rs
Normal file
9
froxy-templates/src/lib.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
#![allow(dead_code)]
|
||||
|
||||
mod templates;
|
||||
|
||||
pub mod friends;
|
||||
pub mod meta_template;
|
||||
pub mod open_modal;
|
||||
pub mod profile;
|
||||
pub mod index;
|
||||
5
froxy-templates/src/meta_template.rs
Normal file
5
froxy-templates/src/meta_template.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
pub use crate::templates::TEMPLATE;
|
||||
|
||||
pub fn add_to_context(env: &mut minijinja::Environment) {
|
||||
env.add_template("template.html", TEMPLATE).unwrap();
|
||||
}
|
||||
5
froxy-templates/src/open_modal.rs
Normal file
5
froxy-templates/src/open_modal.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
pub use crate::templates::OPEN_MODAL as TEMPLATE;
|
||||
|
||||
pub fn add_to_context(env: &mut minijinja::Environment) {
|
||||
env.add_template("open_modal.html", TEMPLATE).unwrap();
|
||||
}
|
||||
43
froxy-templates/src/profile.rs
Normal file
43
froxy-templates/src/profile.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
use smol_str::SmolStr;
|
||||
|
||||
pub use crate::templates::PROFILE as TEMPLATE;
|
||||
|
||||
pub fn add_to_context(env: &mut minijinja::Environment) {
|
||||
if env.get_template("template.html").is_err() {
|
||||
crate::meta_template::add_to_context(env);
|
||||
}
|
||||
if env.get_template("open_modal.html").is_err() {
|
||||
crate::open_modal::add_to_context(env);
|
||||
}
|
||||
|
||||
env.add_template("profile.html", TEMPLATE).unwrap();
|
||||
}
|
||||
|
||||
pub fn render(env: &minijinja::Environment, data: ProfileData) -> Result<String, minijinja::Error> {
|
||||
let template = env.get_template("profile.html")?;
|
||||
template.render(data)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ProfileUser {
|
||||
pub position: Option<SmolStr>,
|
||||
pub last_active: SmolStr,
|
||||
pub image: String,
|
||||
pub name: SmolStr,
|
||||
pub pool: Option<SmolStr>,
|
||||
pub id: u64,
|
||||
pub is_friend: bool,
|
||||
pub is_self: bool,
|
||||
|
||||
pub github: Option<String>,
|
||||
pub website: Option<String>,
|
||||
pub discord: Option<String>,
|
||||
|
||||
/// profile description
|
||||
pub recit: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ProfileData {
|
||||
pub user: ProfileUser,
|
||||
}
|
||||
139
froxy-templates/src/templates/friends.html
Normal file
139
froxy-templates/src/templates/friends.html
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
{% extends 'template.html' %}
|
||||
{% block css %}
|
||||
<link href="/static/css/friends.css?v={{ version }}" rel="stylesheet">
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="modal fade" id="addFriendModal" tabindex="-1" aria-labelledby="addFriendLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="addFriendLabel">Ajouter un ami</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text" id="loginAddon">Login</span>
|
||||
<input autofocus list="suggestions" type="text" class="form-control" id="addFriendInput"
|
||||
aria-label="Login42" aria-describedby="loginAddon" placeholder="ami1, ami2, ami3">
|
||||
<datalist id="suggestions"></datalist>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
|
||||
<button type="button" class="btn btn-primary" id="addFriendButton">
|
||||
<i class="fa-solid fa-user-plus"></i> <i hidden class="spinner-border spinner-border-sm"></i>
|
||||
Ajouter en ami
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'open_modal.html' %}
|
||||
|
||||
<div class="container">
|
||||
<div class="row text-center justify-content-center">
|
||||
{% for friend in friends %}
|
||||
<div id="case-{{ friend.name }}" class="card shadow m-2 pl-fix card-size grow"
|
||||
onclick="openFriend('{{ friend.name }}', true)">
|
||||
<img src="{{ friend.image | safe }}" class="m-1 card-img-top card-img-size"
|
||||
alt="{{ friend.name }}'s image">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class="fa-solid {{ "fa-2xs fa-circle online" if friend.position else "fa-xs fa-person-walking offline" }}"></i> {{ friend.name }}
|
||||
</h5>
|
||||
<p class="card-text">{{ friend.position if friend.position else 'Absent' }} {{ friend.last_active }}
|
||||
{% if friend.position %}
|
||||
<a class="fa-solid fa-users-viewfinder" href="/goto/{{ friend.position }}"></a>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if add %}
|
||||
<div class="card p-0 m-2 shadow card-size grow" onclick="openAddFriend();">
|
||||
<div class="m-1 w-100 card-img-top text-center card-not-img-size">
|
||||
<i class="fa-solid fa-plus fa-5x"></i>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Ajouter un ami</h5>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
let openAddFriendModal;
|
||||
let openFriendModal;
|
||||
let addFriendInput = document.getElementById('addFriendInput');
|
||||
let addFriendButton = document.getElementById('addFriendButton');
|
||||
|
||||
function fillSuggestions(element, suggestions) {
|
||||
let list = document.querySelector(element);
|
||||
|
||||
list.innerHTML = ''
|
||||
suggestions.forEach(function (item) {
|
||||
if (item['type'] !== "user") return;
|
||||
let option = document.createElement('option');
|
||||
option.value = item['v'];
|
||||
list.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
function openAddFriend() {
|
||||
const modal = new bootstrap.Modal('#addFriendModal', {});
|
||||
openAddFriendModal = modal;
|
||||
modal.show();
|
||||
setTimeout(() => {
|
||||
addFriendInput.focus();
|
||||
addFriendInput.select();
|
||||
}, 500)
|
||||
}
|
||||
|
||||
async function deleteLocalFriend() {
|
||||
let friend_name = document.getElementById('openFriendLabel').innerText.trim();
|
||||
let resp = await deleteFriend(friend_name, "#deleteFriend")
|
||||
if (resp === 200) {
|
||||
openFriendModal.hide();
|
||||
document.getElementById('case-' + friend_name).remove();
|
||||
}
|
||||
}
|
||||
|
||||
addFriendInput.addEventListener('keyup', function (key) {
|
||||
let name = addFriendInput.value.trim().toLowerCase();
|
||||
|
||||
if (key.key === 'Enter') {
|
||||
if (name.length <= 3) {
|
||||
addFriendInput.focus();
|
||||
return;
|
||||
}
|
||||
addFriend(name, '#addFriendButton', true);
|
||||
fillSuggestions('#suggestions', []);
|
||||
}
|
||||
if (name.trim() === '') return fillSuggestions('#suggestions', []);
|
||||
if (name.length < 3) return;
|
||||
setTimeout(() => {
|
||||
if (name !== addFriendInput.value) return;
|
||||
fetch('/search/' + encodeURIComponent(name) + "/1").then((response) => {
|
||||
response.json().then((json) => {
|
||||
fillSuggestions('#suggestions', json)
|
||||
})
|
||||
})
|
||||
}, 200)
|
||||
})
|
||||
|
||||
addFriendButton.addEventListener('click', function () {
|
||||
let val = addFriendInput.value.trim().toLowerCase();
|
||||
if (val.length <= 3) {
|
||||
addFriendInput.focus();
|
||||
return;
|
||||
}
|
||||
addFriend(val, '#addFriendButton', true)
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
228
froxy-templates/src/templates/index.html
Normal file
228
froxy-templates/src/templates/index.html
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
{% extends 'template.html' %}
|
||||
{% block css %}
|
||||
<link href="/static/css/friends.css?v={{ version }}" rel="stylesheet">
|
||||
<link href="/static/css/index.css?v={{ version }}" rel="stylesheet">
|
||||
<style>
|
||||
@media (max-width: 800px) {
|
||||
.btn-width {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-width {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.range-no-color {
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
.btn-check:checked+.btn-outline-warning > .text-muted {
|
||||
color: black!important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
{% include 'open_modal.html' %}
|
||||
<div class="modal fade" id="issueModal" tabindex="-1" aria-labelledby="issueModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="issueModalLabel">Signalement dumps</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close report"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
Le dump <span id="dumpIdIssueModal"></span> ne fonctionne pas comme prévu ? Signalez le problème
|
||||
pour avertir que la place n'est pas
|
||||
libre
|
||||
<div class="form-check" id="issueForm">
|
||||
<input class="form-check-input" type="radio" data-id="1" name="issueRadios" id="issueRadios1">
|
||||
<label class="form-check-label" for="issueRadios1">
|
||||
Le dump est inutilisable
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" data-id="2" name="issueRadios" id="issueRadios2">
|
||||
<label class="form-check-label" for="issueRadios2">
|
||||
Le dump fonctionne, mais l'écran ou le clavier est dégradé
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
|
||||
<button type="button" class="btn btn-primary" id="submitIssue">Valider</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<div class="btn-group mb-1 btn-width" role="group" aria-label="Change cluster">
|
||||
{% for cluster in clusters %}
|
||||
<input type="radio" class="btn-check cluster_radios" name="btn-radio"
|
||||
data-cluster="{{ cluster.name }}" id="radio{{ cluster.name }}"
|
||||
autocomplete="off" {{ "checked" if actual_cluster == cluster['name'] else "" }}>
|
||||
<label title="{{ (int(cluster.users / (cluster.maximum_places - cluster.dead_pc) * 100) if (cluster.maximum_places - cluster.dead_pc) > 0 else 0 ) }}%" class="btn btn-outline-{{ place_to_btn(cluster) }} m-t" for="radio{{ cluster.name }}">
|
||||
{{ ('<i class="fa-solid fa-person-swimming text-info"></i>' if cluster.name in piscine else '') | safe }}{{ (' ' if cluster.name in piscine and cluster.name in silent else '') | safe }}{{ ('<i class="fa-solid fa-volume-xmark text-secondary"></i> ' if cluster.name in silent else '') | safe }}{{ cluster.name }}
|
||||
<span class="text-muted">{{ cluster.users }}/{{ cluster.maximum_places - cluster.dead_pc }}</span></label>
|
||||
{%- endfor -%}
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-fluid mt-2 scroll p-0">
|
||||
<table class="grid text-center">
|
||||
<tbody>
|
||||
|
||||
{% for index, x in enumerate(map[1:], 0) %}
|
||||
<tr>
|
||||
{%- set countB = namespace(value=1) -%}
|
||||
<td class="range">{{ map[0][index] }}</td>
|
||||
{%- for y in x -%}
|
||||
{%- if y == 'x' -%}
|
||||
<td class="range"> </td>
|
||||
{%- elif y == '|' -%}
|
||||
<td class="range-no-color"></td>
|
||||
{%- elif y == 'h' -%}
|
||||
<td></td>
|
||||
{%- elif y == 'd' -%}
|
||||
<td><i class="fa-solid fa-person-walking fa-2xl"></i></td>
|
||||
{%- elif y == 'f' -%}
|
||||
<td><i class="fa-solid fa-laptop fa-xl"></i></td>
|
||||
{%- elif exrypz(y) == False -%}
|
||||
<td class="small-colors"></td>
|
||||
{%- else -%}
|
||||
{%- set text_color = '' -%}
|
||||
{%- set img = '<i class="fa-solid fa-user"></i>' -%}
|
||||
{%- if y in locations -%}
|
||||
{%- set img = '<img loading="lazy" class="profile-pic2" alt="" src="' + (custom_image(locations[y]['user']['login']) if locations[y]['has_custom_image'] else proxy_images(locations[y]['user']['image']['versions']['small'])) + '">' -%}
|
||||
{%- elif actual_cluster in piscine and y in tutor_station -%}
|
||||
{%- set img = '<i class="fa-solid fa-shield tutor-shield"></i>' -%}
|
||||
{%- endif -%}
|
||||
|
||||
{%- if actual_cluster in piscine and y in tutor_station -%}
|
||||
{%- set text_color = 'tutor-shield' -%}
|
||||
{%- endif -%}
|
||||
|
||||
{%- set relation = '' -%}
|
||||
{%- set currently_in_piscine = y in locations and (locations[y]['user']['pool_month'], locations[y]['user']['pool_year']) in piscine_date -%}
|
||||
{%- set in_piscine_cluster = actual_cluster in piscine -%}
|
||||
{%- if y in locations and locations[y]['user']['id'] not in tutors and currently_in_piscine != in_piscine_cluster -%}
|
||||
{%- set relation = 'wrong-cluster' -%}
|
||||
{%- endif -%}
|
||||
{%- if focus and focus == y %}
|
||||
{%- set relation = 'focus' -%}
|
||||
{%- elif y in locations and locations[y]['me'] -%}
|
||||
{%- set relation = 'me' -%}
|
||||
{%- elif y in locations and locations[y]['whitelist'] -%}
|
||||
{%- set relation = 'whitelist' -%}
|
||||
{%- elif y in locations and locations[y]['close_friend'] -%}
|
||||
{%- set relation = 'close_friend' -%}
|
||||
{%- elif y in locations and locations[y]['friend'] -%}
|
||||
{%- set relation = 'friend' -%}
|
||||
{%- elif y in issues_map and issues_map[y]['count'] >= 1 and issues_map[y]['issue'] == 1 -%}
|
||||
{%- set relation = 'dead' -%}
|
||||
{%- elif y in issues_map and issues_map[y]['count'] >= 1 and issues_map[y]['issue'] == 2 -%}
|
||||
{%- set relation = 'attention' -%}
|
||||
{%- elif y in locations and locations[y]['pool'] and actual_cluster not in piscine -%}
|
||||
{%- set relation = 'pooled' -%}
|
||||
{%- endif -%}
|
||||
|
||||
{%- if actual_cluster in piscine and y in locations and locations[y]['user']['id'] in tutors -%}
|
||||
{%- set relation = 'tutor' -%}
|
||||
{%- set text_color = '' -%}
|
||||
{%- elif actual_cluster in piscine and y in locations and not locations[y]['user']['tutor'] in tutors and y in tutor_station -%}
|
||||
{%- set relation = 'tutor-spot' -%}
|
||||
{%- set text_color = '' -%}
|
||||
{%- endif -%}
|
||||
{%- if y in locations and locations[y]['admin'] -%}
|
||||
{%- set relation = 'admin' -%}
|
||||
{%- endif -%}
|
||||
|
||||
{%- if countB.value % 2 == 0 -%}
|
||||
<td data-pos="{{ y }}"
|
||||
data-login="{{ locations[y]['user']['login'] if y in locations else '' }}"
|
||||
class="sm-t case {{ relation }} {{ text_color }}">{{ img|safe }}<br>{{ y.replace(actual_cluster, '') }}
|
||||
</td>
|
||||
{%- else -%}
|
||||
<td data-pos="{{ y }}"
|
||||
data-login="{{ locations[y]['user']['login'] if y in locations else '' }}"
|
||||
class="sm-t case {{ relation }} {{ text_color }}">{{ y.replace(actual_cluster, '') }}<br>{{ img|safe }}
|
||||
</td>
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
{%- set countB.value = countB.value + 1 -%}
|
||||
{%- endfor -%}
|
||||
<td class="range">{{ map[0][index] }}</td>
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
</tbody>
|
||||
</table>
|
||||
<div id="tooltip" hidden class="rounded bg-white zindex9999">
|
||||
<img class="profile-pic rounded no-shadow" alt="pp">
|
||||
<div class="fluid-container text-center text-black name">name</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/popper.min.js?v={{ version }}" defer></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
let tooltip = document.getElementById('tooltip')
|
||||
let cases = document.querySelectorAll('.case');
|
||||
cases.forEach(e => {
|
||||
e.addEventListener('click', () => {
|
||||
let login = e.dataset.login
|
||||
let dump = e.dataset.pos
|
||||
if (login.length === 0) {
|
||||
const modal = new bootstrap.Modal('#issueModal', {});
|
||||
document.getElementById('submitIssue').onclick = async function () {
|
||||
let checked = document.querySelector('input[name="issueRadios"]:checked');
|
||||
if (!checked) {
|
||||
document.getElementById('issueRadios2').focus()
|
||||
return;
|
||||
}
|
||||
let resp = checked.dataset.id;
|
||||
fetch(`/addissue/${dump}/${resp}`).then(resp => {
|
||||
if (resp.status === 200) {
|
||||
triggerToast('Merci de votre signalement ! :)', true, false);
|
||||
modal.hide();
|
||||
} else {
|
||||
triggerToast("Une erreur s'est produite lors du signalement", false, false);
|
||||
}
|
||||
})
|
||||
}
|
||||
document.getElementById('dumpIdIssueModal').innerText = dump;
|
||||
modal.show();
|
||||
return;
|
||||
}
|
||||
openFriend(login, true)
|
||||
})
|
||||
|
||||
if (!e.dataset.login)
|
||||
return;
|
||||
e.addEventListener('mouseover', () => {
|
||||
if (window.isTouchDevice && isTouchDevice()) return;
|
||||
if (!window.Popper) return;
|
||||
tooltip.hidden = false;
|
||||
tooltip.querySelector('img').src = e.querySelector('img').src
|
||||
tooltip.querySelector('.name').innerText = e.dataset.login
|
||||
Popper.createPopper(e, tooltip, {
|
||||
placement: 'right'
|
||||
})
|
||||
})
|
||||
|
||||
e.addEventListener('mouseleave', () => {
|
||||
tooltip.hidden = true;
|
||||
});
|
||||
});
|
||||
|
||||
// Change cluster buttons
|
||||
let radio_clusters = document.querySelectorAll('.cluster_radios')
|
||||
radio_clusters.forEach(e => {
|
||||
e.addEventListener('click', () => {
|
||||
let cluster = e.dataset.cluster
|
||||
location.href = "/?cluster=" + cluster
|
||||
})
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
11
froxy-templates/src/templates/mod.rs
Normal file
11
froxy-templates/src/templates/mod.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
macro_rules! template {
|
||||
($name:ident, $path:literal) => {
|
||||
pub static $name: &'static str = include_str!($path);
|
||||
};
|
||||
}
|
||||
|
||||
template!(FRIENDS, "./friends.html");
|
||||
template!(INDEX, "./index.html");
|
||||
template!(OPEN_MODAL, "./open_modal.html");
|
||||
template!(PROFILE, "./profile.html");
|
||||
template!(TEMPLATE, "./template.html");
|
||||
76
froxy-templates/src/templates/open_modal.html
Normal file
76
froxy-templates/src/templates/open_modal.html
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
<div class="modal fade" id="openFriendModal" tabindex="-1" aria-labelledby="openFriendLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5 me-2" id="openFriendLabel"></h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<!-- PP - Name - Pool -->
|
||||
<div class="modal-body">
|
||||
<div class="mb-3 text-center">
|
||||
<img alt="" class="rounded-pill profile-pic" style="height:9rem" id="openFriendModalPic" src="">
|
||||
<div class="display-inline-grid ms-3">
|
||||
<span id="openFriendModalName">
|
||||
<h5 class="name display-5 fw-bold"></h5>
|
||||
<span id="addCloseFriend">
|
||||
<i class="fa-regular fa-star text-warning"></i>
|
||||
<i hidden class="spinner-border spinner-border-sm"></i>
|
||||
</span>
|
||||
<span id="removeCloseFriend">
|
||||
<i class="fa-solid fa-star text-warning"></i>
|
||||
<i hidden class="spinner-border spinner-border-sm"></i>
|
||||
</span>
|
||||
<span class="fs-6 text-muted pool">Piscine de</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Bio & Socials -->
|
||||
<div hidden class="rounded bg-dark-subtle m-2 p-1" id="FP-bio">
|
||||
<div hidden class="row text-center" id="FP-socials-row">
|
||||
<div hidden class="col no-wrap" id="FP-github">
|
||||
<i class="fa-brands fa-github icon-length"></i>
|
||||
<a class="{{ 'no-click' if kiosk else '' }}" href=""></a>
|
||||
</div>
|
||||
<div hidden class="col no-wrap" id="FP-website">
|
||||
<i class="fa-solid fa-globe icon-length"></i>
|
||||
<a class="{{ 'no-click' if kiosk else '' }}" href=""></a>
|
||||
</div>
|
||||
<div hidden class="col no-wrap" id="FP-discord">
|
||||
<i class="fa-brands fa-discord icon-length"></i> <a></a>
|
||||
</div>
|
||||
</div>
|
||||
<div hidden class="div text-center m-1 justify" style="text-align: justify" id="FP-text"></div>
|
||||
</div>
|
||||
<!-- Buttons -->
|
||||
<div class="row text-center">
|
||||
<div class="col-md mb-1">
|
||||
<button type="button" class="btn btn-sm btn-primary" id="openFriendShowCluster">
|
||||
<i class="fa-solid fa-magnifying-glass"></i> Afficher dans le Cluster
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md mb-1">
|
||||
<a id="openFriendProfile">
|
||||
<button type="button" class="btn btn-sm btn-secondary">
|
||||
<i class="fa-solid fa-circle-info"></i> Voir l'intra de l'utilisateur
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md mb-1">
|
||||
<button hidden type="button" class="btn btn-sm btn-danger" id="openFriendLabelDeleteFriend">
|
||||
<i class="fa-solid fa-user-minus"></i>
|
||||
<i hidden class="spinner-border spinner-border-sm"></i>
|
||||
Supprimer des contacts
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-success" id="openFriendLabelAddFriend">
|
||||
<i class="fa-solid fa-user-plus"></i><i hidden class="spinner-border spinner-border-sm"></i>
|
||||
Ajouter au contacts
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
111
froxy-templates/src/templates/profile.html
Normal file
111
froxy-templates/src/templates/profile.html
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
{% extends 'template.html' %}
|
||||
{% block css %}
|
||||
<style>
|
||||
.align-vertical {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.badge-sm {
|
||||
font-size: 1rem;
|
||||
}
|
||||
</style>
|
||||
<link href="/static/css/friends.css?v={{ version }}" rel="stylesheet">
|
||||
<link href="/static/css/profile.css?v={{ version }}" rel="stylesheet">
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
{% include 'open_modal.html' %}
|
||||
<div class="container mt-2">
|
||||
<div class="p-5 mb-4 bg-light-subtle rounded-3 shadow-sm">
|
||||
<div class="container-fluid py-5">
|
||||
<div class="mb-4 text-center">
|
||||
<img class="rounded-pill profile-pic" style="height:9rem" src="{{ user.image }}" alt="{{ user.name }}'s pic">
|
||||
<div class="display-inline-grid ms-3">
|
||||
<h1 class="display-5 fw-bold">
|
||||
{{ user.name }}
|
||||
{% if 'None' not in user.pool %}<span class="fs-6 text-muted ">Piscine de {{ user.pool }}</span>{% endif %}
|
||||
</h1>
|
||||
<span class="text-left">
|
||||
<i class="fa-solid {{ "fa-2=xs fa-circle online" if user.position else "fa-xs fa-person-walking offline" }}"></i>
|
||||
{{ user.position if user.position else 'Absent' }} {{ user.last_active }}
|
||||
{% if user.position %}<a class="fa-solid fa-users-viewfinder" href="/goto/{{ user.position }}"></a>{% endif %}
|
||||
</span>
|
||||
<span class="text-left">
|
||||
{% if !user.is_self %}
|
||||
<button {{ 'hidden' if user.is_friend else '' }}
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline-success online"
|
||||
id="addLocalFriend">
|
||||
<i class="fa-solid fa-user-plus"></i>
|
||||
<i hidden class="spinner-border spinner-border-sm"></i>
|
||||
Ajouter {{ user.name }} en ami
|
||||
</button>
|
||||
<button {{ 'hidden' if not user.is_friend else '' }}
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline-danger"
|
||||
id="removeLocalFriend">
|
||||
<i class="fa-solid fa-user-minus"></i>
|
||||
<i hidden class="spinner-border spinner-border-sm"></i>
|
||||
Retirer {{ user.name }} des amis
|
||||
</button>
|
||||
{% endif %}
|
||||
<button type="button"
|
||||
onclick="newTab('https://profile.intra.42.fr/users/{{ user.name }}');"
|
||||
class="btn btn-sm btn-outline-secondary">
|
||||
<i class="fa-solid fa-circle-info"></i>
|
||||
Profil intra de {{ user.name }}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fs-5">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
{%- if user.github or user.website or user.discord -%}
|
||||
<p class="mb-0">Contacts</p>
|
||||
<ul>
|
||||
{%- if 'github' in user and len(user.github) >= 1 -%}
|
||||
<li>
|
||||
<i class="fa-brands fa-github icon-length"></i> <a href="{{ user.github }}">{{ user.github.replace('https://github.com/', '') }}</a>
|
||||
</li>
|
||||
{%- endif -%}
|
||||
{%- if 'website' in user and len(user.website) >= 1 -%}
|
||||
<li>
|
||||
<i class="fa-solid fa-globe icon-length"></i> <a href="{{ user.website }}">{{ user.website.replace('https://', '').replace("http://", "") }}</a>
|
||||
</li>
|
||||
{%- endif -%}
|
||||
{%- if 'discord' and len(user.discord) >= 1 -%}
|
||||
<li>
|
||||
<i class="fa-brands fa-discord icon-length"></i> <a>{{ user.discord }}</a>
|
||||
</li>
|
||||
{%- endif -%}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-8" style="text-align: justify;">{{ user.recit if user.recit and len(user.recit) > 0 else '' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block scripts %}
|
||||
<script>
|
||||
let addLocalFriend = document.getElementById('addLocalFriend');
|
||||
let removeLocalFriend = document.getElementById('removeLocalFriend');
|
||||
|
||||
if (addLocalFriend)
|
||||
addLocalFriend.addEventListener('click', async () => {
|
||||
await addFriend('{{ user.name }}', '#addLocalFriend');
|
||||
addLocalFriend.hidden = true;
|
||||
removeLocalFriend.hidden = false;
|
||||
})
|
||||
|
||||
if (removeLocalFriend)
|
||||
removeLocalFriend.addEventListener('click', async () => {
|
||||
await deleteFriend('{{ user.name }}', '#removeLocalFriend');
|
||||
addLocalFriend.hidden = false;
|
||||
removeLocalFriend.hidden = true;
|
||||
})
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
81
froxy-templates/src/templates/template.html
Normal file
81
froxy-templates/src/templates/template.html
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
{%- set version='9' %}
|
||||
<!doctype html>
|
||||
<html lang="fr" data-bs-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>FFT - Maix.me</title>
|
||||
<link href="/static/css/bootstrap.min.css?v={{ version }}" rel="stylesheet">
|
||||
<link href="/static/css/common.css?v={{ version }}" rel="stylesheet">
|
||||
<link rel="manifest" href="/static/manifest.json?v={{ version }}">
|
||||
<link rel="shortcut icon" type="image/x-icon" href="/static/img/favicon.ico">
|
||||
<link rel="apple-touch-icon" href="/static/img/apple-touch-icon.png"/>
|
||||
<meta name="theme-color" content="rgb(43, 48, 53)"/>
|
||||
{% block css %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="navbar navbar-expand-lg bg-body-tertiary mb-2 shadow">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand me-2 position-relative " href="/">
|
||||
<img src="/static/img/android-chrome-192x192.png" alt="Logo" width="24" height="24" class="d-inline-block align-text-top">
|
||||
FFT<span class="text-muted text-beta"></span>
|
||||
</a>
|
||||
<button class="btn btn-secondary hide-navbar" hidden id="qc-friends">
|
||||
<i class="fa-solid fa-user-group"></i>
|
||||
</button>
|
||||
<button class="btn btn-secondary hide-navbar" hidden id="qc-cluster">
|
||||
<i class="fa-solid fa-layer-group"></i>
|
||||
</button>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
|
||||
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" aria-current="page" href="/">Clusters</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" aria-current="page" href="/friends/">Amis</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="d-flex" role="search">
|
||||
<button type="button" class="btn btn-outline-secondary me-1" onclick="newTab('https://github.com/maix0/fft');" aria-label="Github">
|
||||
<i class="fa-brands fa-github"></i> <i hidden class="spinner-border spinner-border-sm"></i>
|
||||
</button>
|
||||
<input class="form-control" list="global_suggestions" id="globalSearch" type="search" placeholder="Rechercher..." aria-label="Search">
|
||||
<datalist id="global_suggestions"></datalist>
|
||||
<button type="button" id="globalSearchButton" aria-label="Search" class="btn btn-outline-light mx-1">
|
||||
<i class="fa-solid fa-magnifying-glass"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-light" aria-label="Settings"
|
||||
onclick="location.href = '/settings/';">
|
||||
<i class="fa-solid fa-gear"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="toast-container position-fixed top-0 end-0 p-3">
|
||||
<div id="liveToast" class="toast align-items-center text-bg-danger border-0" role="alert" aria-live="assertive"
|
||||
aria-atomic="true">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body" id="toast_body">
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"
|
||||
aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% block content %}{% endblock %}
|
||||
<script async src="/static/js/bootstrap.min.js?v={{ version }}"></script>
|
||||
<script src="/static/js/common.js?v={{ version }}"></script>
|
||||
{% block scripts %}{% endblock %}
|
||||
|
||||
<link href="/static/fontawesome/css/fontawesome.min.css?v={{ version }}" rel="stylesheet">
|
||||
<link href="/static/fontawesome/css/solid.min.css?v={{ version }}" rel="stylesheet">
|
||||
<link href="/static/fontawesome/css/regular.min.css?v={{ version }}" rel="stylesheet">
|
||||
<link href="/static/fontawesome/css/brands.min.css?v={{ version }}" rel="stylesheet">
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue