Initial commit - Bolsa app avec Tailwind et authentification

This commit is contained in:
2026-07-22 16:37:53 +02:00
commit 2057d693cd
14 changed files with 594 additions and 0 deletions
+204
View File
@@ -0,0 +1,204 @@
<!doctype html>
<html lang="fr" class="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{% block title %}Bolsa App{% endblock %}</title>
<!-- Favicon -->
<link
rel="icon"
type="image/png"
href="{{ url_for('static', filename='images/favicon.png') }}"
/>
<!-- Tailwind CSS CDN -->
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
</head>
<body
class="bg-gray-950 text-gray-100 min-h-screen flex flex-col justify-between"
>
<!-- Header Global -->
<header
class="bg-gray-900 border-b border-gray-800 px-6 py-4 flex items-center justify-between"
>
<!-- Gauche : Logo (taille augmentée d'environ 1.5x, ex: h-10 -> h-16) -->
<div class="flex items-center">
<a
href="{{ url_for('main.menu') if session.get('user_id') else url_for('main.index') }}"
>
<img
src="{{ url_for('static', filename='images/GFBlancLogo.png') }}"
alt="Logo Giraud Finance"
class="h-16 w-auto"
/>
</a>
</div>
<!-- Centre : Titre (agrandi avec text-4xl et police plus grasse) -->
<div class="absolute left-1/2 transform -translate-x-1/2">
<h1 class="text-4xl font-extrabold tracking-wider text-white">Bolsa</h1>
</div>
<!-- Droite : Horloges Paris & New-York + Lien de déconnexion -->
<div class="text-xs font-mono text-right space-y-1">
<div id="clock-paris" class="flex items-center justify-end space-x-2">
<span>Paris : <span class="time">--:--:--</span></span>
<span
id="status-paris"
class="inline-block w-2.5 h-2.5 rounded-full bg-red-500"
title="Fermée"
></span>
</div>
<div id="clock-ny" class="flex items-center justify-end space-x-2">
<span>New-York : <span class="time">--:--:--</span></span>
<span
id="status-ny"
class="inline-block w-2.5 h-2.5 rounded-full bg-red-500"
title="Fermée"
></span>
</div>
{% if session.get('user_id') %}
<div class="pt-1.5 flex justify-end">
<a
href="{{ url_for('main.logout') }}"
class="no-underline px-2.5 py-1 bg-red-950/60 hover:bg-red-900/80 border border-red-900/60 text-red-300 rounded text-[11px] font-sans transition duration-200"
>
Se déconnecter
</a>
</div>
{% endif %}
</div>
</header>
<!-- Container dynamique -->
<main class="flex-grow container mx-auto px-4 py-8">
{% block content %}{% endblock %}
</main>
<!-- Footer Global -->
<footer
class="bg-gray-900 border-t border-gray-800 px-6 py-4 flex items-center justify-between text-xs text-gray-400"
>
<!-- Espace vide à gauche pour équilibrer le flexbox -->
<div class="w-1/3"></div>
<!-- Centre : Copyright -->
<div class="w-1/3 text-center">
<p>copyright © Jean-François GIRAUD - Giraud Finance - 2026</p>
</div>
<!-- Droite : Nom et IP de l'utilisateur si connecté -->
<div class="w-1/3 text-right font-mono">
{% if session.get('user_id') %}
<div class="text-gray-200 font-semibold">
{{ session.get('user_name') }}
</div>
<div id="external-ip" class="text-gray-400 text-[11px]">
{{ session.get('user_ip', 'IP inconnue') }}
</div>
{% endif %}
</div>
</footer>
<!-- Script pour récupérer l'IP externe en JS -->
{% if session.get('user_id') %}
<script>
fetch("https://api.ipify.org?format=json")
.then((response) => response.json())
.then((data) => {
document.getElementById("external-ip").textContent = data.ip;
})
.catch((error) => {
document.getElementById("external-ip").textContent =
"IP non disponible";
});
</script>
{% endif %}
<!-- Script pour l'heure et l'état des bourses -->
<script>
function updateClocks() {
const now = new Date();
// Options pour Paris (Europe/Paris)
const optionsParis = {
timeZone: "Europe/Paris",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
};
const timeStringParis = new Intl.DateTimeFormat(
"fr-FR",
optionsParis,
).format(now);
document.querySelector("#clock-paris .time").textContent =
timeStringParis;
// Options pour New York (America/New_York)
const optionsNY = {
timeZone: "America/New_York",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
};
const timeStringNY = new Intl.DateTimeFormat("fr-FR", optionsNY).format(
now,
);
document.querySelector("#clock-ny .time").textContent = timeStringNY;
// Vérification ouverture bourses (Jours ouvrés Lundi-Vendredi)
const dayParis = new Intl.DateTimeFormat("fr-FR", {
timeZone: "Europe/Paris",
weekday: "short",
}).format(now);
const hourNumParis = parseFloat(
new Intl.DateTimeFormat("fr-FR", {
timeZone: "Europe/Paris",
hour: "numeric",
minute: "numeric",
hour12: false,
})
.format(now)
.replace(":", "."),
);
const dayNY = new Intl.DateTimeFormat("fr-FR", {
timeZone: "America/New_York",
weekday: "short",
}).format(now);
const hourNumNY = parseFloat(
new Intl.DateTimeFormat("fr-FR", {
timeZone: "America/New_York",
hour: "numeric",
minute: "numeric",
hour12: false,
})
.format(now)
.replace(":", "."),
);
// Paris ouvert du Lundi au Vendredi de 09:00 à 17:30
const isParisOpen =
!["sam.", "dim.", "sam", "dim"].includes(dayParis.toLowerCase()) &&
hourNumParis >= 9.0 &&
hourNumParis <= 17.3;
const dotParis = document.getElementById("status-paris");
dotParis.className = `inline-block w-2.5 h-2.5 rounded-full ${isParisOpen ? "bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.6)]" : "bg-red-500"}`;
// New York ouvert du Lundi au Vendredi de 09:30 à 16:00
const isNYOpen =
!["sam.", "dim.", "sam", "dim"].includes(dayNY.toLowerCase()) &&
hourNumNY >= 9.3 &&
hourNumNY <= 16.0;
const dotNY = document.getElementById("status-ny");
dotNY.className = `inline-block w-2.5 h-2.5 rounded-full ${isNYOpen ? "bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.6)]" : "bg-red-500"}`;
}
setInterval(updateClocks, 1000);
updateClocks();
</script>
</body>
</html>
+45
View File
@@ -0,0 +1,45 @@
{% extends "base.html" %} {% block title %}Gestion des utilisateurs - Bolsa{%
endblock %} {% block header_title %}Gestion utilisateurs{% endblock %} {% block
content %}
<div class="flex flex-col items-center justify-center py-6">
<!-- Box centrée principale -->
<div
class="bg-gray-900 border border-gray-800 p-8 rounded-2xl shadow-2xl w-full max-w-4xl"
>
<!-- En-tête de section -->
<div
class="flex justify-between items-center mb-6 border-b border-gray-800 pb-4"
>
<div>
<h2 class="text-2xl font-extrabold text-white">
Administration des utilisateurs
</h2>
<p class="text-gray-400 text-sm">
Gérez les accès et les comptes autorisés de l'application.
</p>
</div>
<a
href="{{ url_for('main.menu') }}"
class="px-4 py-2 bg-gray-800 hover:bg-gray-700 text-gray-200 text-sm font-medium rounded-lg transition duration-200"
>
Retour au menu
</a>
</div>
<!-- Contenu de la page -->
<div class="text-gray-300 py-8 text-center">
<p class="text-gray-400 italic">
Module de gestion des utilisateurs en cours de chargement...
</p>
</div>
<!-- Pied de page interne -->
<div
class="border-t border-gray-800 pt-4 text-xs text-gray-400 italic text-center"
>
Positions en bourse : Accès sécurisé. Veillez à fermer votre session après
chaque utilisation.
</div>
</div>
</div>
{% endblock %}
+58
View File
@@ -0,0 +1,58 @@
{% extends "base.html" %} {% block title %}Connexion - Bolsa{% endblock %} {%
block content %}
<div class="flex justify-center items-center h-[65vh]">
<div
class="bg-gray-900 border border-gray-800 p-8 rounded-xl shadow-2xl w-full max-w-md"
>
<h2 class="text-2xl font-semibold text-center mb-6 text-white">
Connexion
</h2>
<!-- Messages flash d'erreur -->
{% with messages = get_flashed_messages() %} {% if messages %}
<div
class="mb-4 p-3 bg-red-950 border border-red-800 text-red-300 text-sm rounded text-center"
>
{% for message in messages %} {{ message }} {% endfor %}
</div>
{% endif %} {% endwith %}
<form method="POST" action="{{ url_for('main.index') }}" class="space-y-4">
<div>
<label for="login" class="block text-sm font-medium text-gray-300 mb-1"
>Login :</label
>
<input
type="text"
id="login"
name="login"
required
autocomplete="username"
class="w-full px-4 py-2 bg-gray-950 border border-gray-700 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-blue-600"
/>
</div>
<div>
<label for="pass" class="block text-sm font-medium text-gray-300 mb-1"
>Mot de passe :</label
>
<input
type="password"
id="pass"
name="pass"
required
autocomplete="current-password"
class="w-full px-4 py-2 bg-gray-950 border border-gray-700 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-blue-600"
/>
</div>
<button
type="submit"
class="w-full mt-2 py-2.5 px-4 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition duration-200 cursor-pointer"
>
Valider
</button>
</form>
</div>
</div>
{% endblock %}
+73
View File
@@ -0,0 +1,73 @@
{% extends "base.html" %} {% block title %}Menu - Bolsa{% endblock %} {% block
header_title %}Tableau de bord{% endblock %} {% block content %}
<div class="flex flex-col items-center justify-center py-6">
<!-- Box centrée principale -->
<div
class="bg-gray-900 border border-gray-800 p-8 rounded-2xl shadow-2xl w-full max-w-4xl text-center"
>
<!-- Titre et sous-titre -->
<h2 class="text-3xl font-extrabold text-white mb-2">Espace de gestion</h2>
<p class="text-gray-400 text-sm mb-8">
Accédez aux fonctions clés de l'application financière.
</p>
<!-- Grille des 8 gros boutons -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-5 mb-8">
<a
href="#"
class="py-6 px-6 bg-gray-950 hover:bg-blue-600/30 border-2 border-gray-800 hover:border-blue-500 rounded-xl text-white text-lg font-bold tracking-wide transition-all duration-200 flex items-center justify-center shadow-lg hover:scale-[1.01]"
>
1. Actions
</a>
<a
href="#"
class="py-6 px-6 bg-gray-950 hover:bg-blue-600/30 border-2 border-gray-800 hover:border-blue-500 rounded-xl text-white text-lg font-bold tracking-wide transition-all duration-200 flex items-center justify-center shadow-lg hover:scale-[1.01]"
>
2. Ordres
</a>
<a
href="#"
class="py-6 px-6 bg-gray-950 hover:bg-blue-600/30 border-2 border-gray-800 hover:border-blue-500 rounded-xl text-white text-lg font-bold tracking-wide transition-all duration-200 flex items-center justify-center shadow-lg hover:scale-[1.01]"
>
3. Comptes
</a>
<a
href="#"
class="py-6 px-6 bg-gray-950 hover:bg-blue-600/30 border-2 border-gray-800 hover:border-blue-500 rounded-xl text-white text-lg font-bold tracking-wide transition-all duration-200 flex items-center justify-center shadow-lg hover:scale-[1.01]"
>
4. Données perso
</a>
<a
href="{{ url_for('main.gestion_utilisateurs') }}"
class="py-6 px-6 bg-gray-950 hover:bg-blue-600/30 border-2 border-gray-800 hover:border-blue-500 rounded-xl text-white text-lg font-bold tracking-wide transition-all duration-200 flex items-center justify-center shadow-lg hover:scale-[1.01]"
>
5. Gestion utilisateurs
</a>
<a
href="#"
class="py-6 px-6 bg-gray-950 hover:bg-blue-600/30 border-2 border-gray-800 hover:border-blue-500 rounded-xl text-white text-lg font-bold tracking-wide transition-all duration-200 flex items-center justify-center shadow-lg hover:scale-[1.01]"
>
6. Stat historique
</a>
<a
href="#"
class="py-6 px-6 bg-gray-950 hover:bg-blue-600/30 border-2 border-gray-800 hover:border-blue-500 rounded-xl text-white text-lg font-bold tracking-wide transition-all duration-200 flex items-center justify-center shadow-lg hover:scale-[1.01]"
>
7. Import/Export Actions CSV
</a>
<a
href="#"
class="py-6 px-6 bg-gray-950 hover:bg-blue-600/30 border-2 border-gray-800 hover:border-blue-500 rounded-xl text-white text-lg font-bold tracking-wide transition-all duration-200 flex items-center justify-center shadow-lg hover:scale-[1.01]"
>
8. Import historique CSV
</a>
</div>
<!-- Pied de formulaire interne -->
<div class="border-t border-gray-800 pt-4 text-xs text-gray-400 italic">
Positions en bourse : Accès sécurisé. Veillez à fermer votre session après
chaque utilisation.
</div>
</div>
</div>
{% endblock %}