correction config.ts et evolution

This commit is contained in:
2026-02-27 02:13:01 +04:00
parent 1fe15b5907
commit 227d5de22e
8 changed files with 532 additions and 528 deletions

8
package-lock.json generated
View File

@@ -8,7 +8,7 @@
"name": "ambient",
"version": "0.0.1",
"dependencies": {
"astro": "^5.17.1"
"astro": "^5.18.0"
}
},
"node_modules/@astrojs/compiler": {
@@ -1661,9 +1661,9 @@
}
},
"node_modules/astro": {
"version": "5.17.3",
"resolved": "https://registry.npmjs.org/astro/-/astro-5.17.3.tgz",
"integrity": "sha512-69dcfPe8LsHzklwj+hl+vunWUbpMB6pmg35mACjetxbJeUNNys90JaBM8ZiwsPK689SAj/4Zqb1ayaANls9/MA==",
"version": "5.18.0",
"resolved": "https://registry.npmjs.org/astro/-/astro-5.18.0.tgz",
"integrity": "sha512-CHiohwJIS4L0G6/IzE1Fx3dgWqXBCXus/od0eGUfxrZJD2um2pE7ehclMmgL/fXqbU7NfE1Ze2pq34h2QaA6iQ==",
"license": "MIT",
"dependencies": {
"@astrojs/compiler": "^2.13.0",

View File

@@ -9,6 +9,6 @@
"astro": "astro"
},
"dependencies": {
"astro": "^5.17.1"
"astro": "^5.18.0"
}
}
}

View File

@@ -0,0 +1,257 @@
---
// BankCard.astro
const { frontmatter, allHumans } = Astro.props;
const { name, monthly_allowance, type = "FLUX" } = frontmatter;
// --- LOGIQUE DE COULEUR SUBTILE ---
const bankConfigs = {
UpDéjeuner: { color: "#f97316", icon: "🍴" }, // Orange Up
"La Banque Postale": { color: "#137bb1", icon: "🏦" }, // Bleu BP
Sumeria: { color: "#116853", icon: "⚡" }, // Vert d'eau
};
const config = bankConfigs[name as keyof typeof bankConfigs] || {
color: "#85a9bc",
icon: "◈",
};
const color = config.color;
const now = new Date();
// Importe le type si nécessaire (Astro le génère pour toi)
// import type { CollectionEntry } from 'astro:content';
const currentMonthEntries = allHumans.filter((h: any) => {
// Remplace 'any' par 'CollectionEntry<"humans">' si tu veux être strict
const d = new Date(h.data.date);
return (
d.getMonth() === now.getMonth() && d.getFullYear() === now.getFullYear()
);
});
// Somme des flux
// BankCard.astro (Partie Logique)
const totalFlux = currentMonthEntries.reduce((acc: number, entry: any) => {
const flxManifests =
entry.data.manifestations?.filter((m: any) => {
// ON FILTRE : cercle FLX ET la source doit correspondre au nom de la carte
return m.cercle === "FLX" && m.source === name;
}) || [];
return (
acc +
flxManifests.reduce(
(sum: number, m: any) => sum + (Number(m.amount) || 0),
0,
)
);
}, 0);
const percentage = Math.max(
0,
Math.min(100, Math.round((totalFlux / monthly_allowance) * 100)),
);
// État du Hebel
let status = { label: "ABONDANCE", color: "#10b981" };
if (percentage < 20) status = { label: "ÉPUISEMENT", color: "#ef4444" };
else if (percentage < 50) status = { label: "FLUX TENDU", color: "#f59e0b" };
else if (percentage < 85) status = { label: "ÉQUILIBRE", color: "#3b82f6" };
---
<article
class="tcg-card bank-card"
style={`--accent: ${color}; --state-color: ${status.color}`}
>
<div class="card-inner">
<header class="card-header">
<span class="card-name">{name}</span>
<span class="card-cost">{config.icon}</span>
</header>
<div class="visual-container">
<div class="source-gauge">
<svg viewBox="0 0 36 36" class="circular-chart">
<path
class="circle-bg"
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
></path>
<path
class="circle"
stroke-dasharray={`${percentage}, 100`}
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
></path>
<text x="18" y="20.35" class="percentage"
>{percentage}%</text
>
</svg>
</div>
<div class="status-badge">{status.label}</div>
</div>
<div class="card-type-line">
Ressource — {type}
</div>
<div class="card-body">
<div class="flavor-text">
{
percentage > 50
? "Le souffle circule avec aisance."
: "Le flux se raréfie, prudence."
}
</div>
<div class="mechanics">
<div class="stat-row">
<span>Intégrité</span>
<div class="power-bar">
{
Array.from({ length: 5 }).map((_, i) => (
<div
class={`segment ${i < percentage / 20 ? "active" : ""}`}
/>
))
}
</div>
</div>
</div>
</div>
<footer class="card-footer">
<span class="rarity-symbol">✦✦✧</span>
<div class="reserve-level">RÉSERVE {percentage}/100</div>
</footer>
</div>
</article>
<style>
.bank-card {
background: linear-gradient(135deg, #fff 0%, #f8fafc 100%);
color: #1a202c;
border-color: var(
--accent
) !important; /* Bordure subtile de la couleur de la banque */
}
.card-inner {
height: 100%;
padding: 10px;
border-radius: 10px;
display: flex;
flex-direction: column;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #e2e8f0; /* Souligné subtil */
padding-bottom: 5px;
margin-bottom: 10px;
}
.card-name {
font-weight: bold;
font-family: "Philosopher", serif;
font-size: 1.1rem;
color: var(--accent); /* Nom coloré subtilement */
}
.visual-container {
flex: 1;
background: #f1f5f9;
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
gap: 10px;
border: 1px solid rgba(0, 0, 0, 0.05);
}
.circular-chart {
max-width: 100px;
width: 100%;
}
.circle-bg {
fill: none;
stroke: rgba(0, 0, 0, 0.05);
stroke-width: 3.8;
}
.circle {
fill: none;
stroke: var(--accent); /* La jauge prend la couleur de la banque */
stroke-width: 2.8;
stroke-linecap: round;
transition: stroke-dasharray 1s ease;
}
.percentage {
fill: #1a202c;
font-family: "Inter", sans-serif;
font-size: 0.5rem;
text-anchor: middle;
font-weight: bold;
}
.status-badge {
background: var(
--state-color
); /* Garde la couleur d'état (vert/orange/rouge) pour la sécurité */
color: white;
font-size: 0.5rem;
padding: 1px 6px;
border-radius: 4px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.card-type-line {
font-size: 0.7rem;
font-weight: bold;
border-bottom: 1px solid #e2e8f0;
margin: 8px 0;
padding-bottom: 2px;
text-transform: uppercase;
color: #64748b;
}
.flavor-text {
font-size: 0.65rem;
font-style: italic;
color: #94a3b8;
margin-bottom: 10px;
min-height: 1.5rem;
}
.stat-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.75rem;
color: #475569;
}
.power-bar {
display: flex;
gap: 3px;
}
.segment {
width: 14px;
height: 4px;
background: #e2e8f0;
border-radius: 1px;
}
.segment.active {
background: var(--accent); /* Segments de la couleur de la banque */
}
.card-footer {
margin-top: auto;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.6rem;
color: #94a3b8;
}
</style>

View File

@@ -21,22 +21,22 @@ const hygieneScore = health?.somatic?.hygiene_level ?? 5;
// 2. RÉINTÉGRATION DES COMPTAGES
const counts = {
PRO: manifestations?.filter((m) => m.cercle === "PRO").length || 0,
SAN: manifestations?.filter((m) => m.cercle === "SAN").length || 0,
SOC: manifestations?.filter((m) => m.cercle === "SOC").length || 0,
FLX: manifestations?.filter((m) => m.cercle === "FLX").length || 0,
HYG: manifestations?.filter((m) => m.cercle === "HYG").length || 0,
PRO: manifestations?.filter((m: any) => m.cercle === "PRO").length || 0,
SAN: manifestations?.filter((m: any) => m.cercle === "SAN").length || 0,
SOC: manifestations?.filter((m: any) => m.cercle === "SOC").length || 0,
FLX: manifestations?.filter((m: any) => m.cercle === "FLX").length || 0,
HYG: manifestations?.filter((m: any) => m.cercle === "HYG").length || 0,
};
// 3. SCORE D'HARMONIE RAFFINÉ
const rawHarmony =
manifestations?.reduce((acc, m) => {
manifestations?.reduce((acc: number, m: any) => {
if (m.type === "pos") return acc + 1;
if (m.type === "neg") return acc - 1;
return acc;
}, 0) || 0;
const uniqueCercles = new Set(manifestations?.map((m) => m.cercle)).size;
const uniqueCercles = new Set(manifestations?.map((m: any) => m.cercle)).size;
const hasAllCercles = uniqueCercles === 5;
const harmonyScore = rawHarmony + (hasAllCercles ? 2 : 0);
@@ -128,16 +128,16 @@ const biomes = {
weak: "OCEAN",
},
};
const currentBiome = biomes[autoBiome] || biomes.SAVANE;
const currentBiome = biomes[autoBiome as keyof typeof biomes] || biomes.SAVANE;
const circleSummaries = Object.entries(counts)
.filter(([_, count]) => count > 0)
.map(([type, count]) => {
.filter(([_, count]: [string, number]) => count > 0)
.map(([type, count]: [string, number]) => {
// Calcul du score de dominance pour ce cercle précis
const circleScore =
manifestations
?.filter((m) => m.cercle === type)
.reduce((acc, m) => {
?.filter((m: any) => m.cercle === type)
.reduce((acc: number, m: any) => {
if (m.type === "pos") return acc + 1;
if (m.type === "neg") return acc - 1;
return acc;

View File

@@ -14,7 +14,6 @@ const {
image,
} = frontmatter;
// Définition des couleurs par catégorie
const categoryColors = {
Festival: "#f59e0b",
IRL: "#a1be18",
@@ -27,274 +26,171 @@ const categoryColors = {
default: "#ec4899",
};
const accentColor = categoryColors[type] || categoryColors["default"];
const accentColor =
categoryColors[type as keyof typeof categoryColors] ||
categoryColors["default"];
// --- LOGIQUE DE NOTIFICATION DE STATUT ---
// --- LOGIQUE DE STATUT ---
const today = new Date();
const eventDate = new Date(date);
// Mise à zéro des heures pour comparer uniquement les jours
today.setHours(0, 0, 0, 0);
const compareDate = new Date(eventDate);
compareDate.setHours(0, 0, 0, 0);
const diffTime = compareDate - today;
// On utilise .getTime() pour transformer les dates en nombres (ms)
const diffTime = compareDate.getTime() - today.getTime();
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
let dynamicStatus = { label: status, color: "rgba(255,255,255,0.3)", icon: "" };
let dynamicStatus = { label: status, color: "rgba(0,0,0,0.3)", icon: "" };
if (status === "Terminé") {
dynamicStatus = { label: "Terminé", color: "#10b981" };
} else if (status === "En cours") {
// Nouvelle condition pour le statut "En cours"
if (status === "Terminé")
dynamicStatus = { label: "Terminé", color: "#10b981", icon: "" }; // Ajoute icon: ""
else if (status === "En cours")
dynamicStatus = { label: "En cours", color: "#3b82f6", icon: "⚡" };
} else if (diffDays < 0) {
dynamicStatus = { label: "En retard", color: "#ef4444" };
} else if (diffDays === 0) {
dynamicStatus = { label: "Aujourd'hui", color: "#3b82f6" };
} else if (diffDays > 0 && diffDays <= 7) {
dynamicStatus = { label: "Imminent", color: "#f59e0b" };
}
else if (diffDays < 0)
dynamicStatus = { label: "En retard", color: "#ef4444", icon: "" }; // Ajoute icon: ""
else if (diffDays === 0)
dynamicStatus = { label: "Aujourd'hui", color: "#3b82f6", icon: "" }; // Ajoute icon: ""
else if (diffDays > 0 && diffDays <= 7)
dynamicStatus = { label: "Imminent", color: "#f59e0b", icon: "" }; // Ajoute icon: ""
---
<article
class={`event-card ${image ? "has-bg" : ""}`}
style={`--accent-color: ${accentColor}; --bg-image: url('${image}'); --status-bg: ${dynamicStatus.color};`}
class={`tcg-card ${image ? "is-dark has-bg" : ""}`}
style={`background-color: ${accentColor}; --bg-image: url('${image}');`}
>
<div class="card-inner">
<header class="event-header">
<span class="event-type">{type.toUpperCase()}</span>
<span class="status-notification">
{
dynamicStatus.icon && (
<span class="status-icon">{dynamicStatus.icon}</span>
)
}
<div class="card-border">
<header class="card-header">
<span class="event-type-badge">{type.toUpperCase()}</span>
<span
class="status-badge"
style={`background: ${dynamicStatus.color}`}
>
{dynamicStatus.icon}
{dynamicStatus.label}
</span>
</header>
<div class="event-art">
<div class="location-tag">{location}</div>
<h2 class="event-title">{title}</h2>
<div class="card-art event-visual">
<div class="location-overlay">{location}</div>
<h2 class="event-title-main">{title}</h2>
</div>
<div class="event-info-line">
Prévu le : <strong
>{new Date(date).toLocaleDateString("fr-FR")}</strong
>
<div class="card-type-line">
Événement — {new Date(date).toLocaleDateString("fr-FR")}
</div>
<div class="event-body">
<p class="summary">{summary}</p>
<div class="card-body">
<p class="summary-text">{summary}</p>
<div class="participants-box">
<span class="label">
<div class="participants-section">
<span class="mini-label"
>{type === "Festival" ? "Artistes :" : "Membres :"}</span
>
<div class="participant-tags">
{
type === "Festival"
? "Artistes invités :"
: "Membres impliqués :"
participants.map((p: string) => (
<span class="p-tag">{p}</span>
))
}
</span>
<div class="tags">
{participants.map((p) => <span class="p-tag">{p}</span>)}
</div>
</div>
</div>
<footer class="event-footer">
<div class="date-recorded">
Projet détecté le : <strong>{target_date}</strong>
</div>
<div class="social-impact">{circle}</div>
<footer class="card-footer">
<div class="footer-meta">Détecté : {target_date}</div>
<div class="circle-badge">{circle}</div>
</footer>
</div>
</article>
<style>
.event-card {
width: 320px;
height: 480px;
background-color: var(--accent-color);
border-radius: 18px;
padding: 12px;
font-family: "Philosopher", serif;
color: rgba(0, 0, 0, 0.7);
transition: background 0.3s ease;
position: relative;
background-size: cover;
background-position: center;
}
.event-card.has-bg {
color: #fff;
text-shadow: 1px 1px 1px rgba(0, 0, 0, 1);
background-image:
linear-gradient(
to bottom,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0) 100%
),
var(--bg-image) !important;
}
.card-inner {
height: 100%;
border-radius: 10px;
display: flex;
/* On surcharge uniquement ce qui est spécifique à l'événement */
.event-visual {
flex-direction: column;
padding: 12px;
background: linear-gradient(
rgba(20, 0, 255, 0.1) 0%,
rgba(255, 191, 0, 0.1) 70%,
rgba(255, 0, 0, 0.3) 100%
);
}
.event-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.7rem;
font-weight: bold;
letter-spacing: 1px;
}
/* Style du nouveau badge de notification */
.status-notification {
background: var(--status-bg);
color: white;
padding: 2px 10px;
border-radius: 5px;
font-size: 0.6rem;
text-transform: uppercase;
display: flex;
align-items: center;
gap: 5px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
text-shadow: none;
}
@keyframes blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.event-art {
flex: 1;
border: 1px solid rgba(0, 0, 0, 0.1);
margin: 10px 0;
border-radius: 6px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: rgba(0, 0, 0, 0.05);
text-align: center;
padding: 10px;
}
.event-card.has-bg .event-art {
border: 1px solid rgba(0, 0, 0, 0.1);
.has-bg .card-border {
background: rgba(0, 0, 0, 0.4); /* Voile noir transparent */
backdrop-filter: blur(
2px
); /* Optionnel : floute légèrement l'image derrière le texte */
color: white;
}
.event-title {
font-size: 1.4rem;
.event-title-main {
font-size: 1.2rem;
margin: 5px 0;
line-height: 1.1;
font-family: "Philosopher", serif;
font-weight: bold;
color: inherit; /* Utilise la couleur parente (blanc ou sombre) */
}
.location-tag {
font-size: 0.7rem;
opacity: 0.9;
}
.event-card.has-bg .location-tag {
opacity: 1;
}
.event-info-line {
font-size: 0.8rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
padding-bottom: 5px;
margin-bottom: 10px;
}
.event-card.has-bg .event-info-line {
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
.event-body {
flex: 1.2;
font-size: 0.85rem;
}
.event-type {
text-transform: uppercase;
background: rgba(255, 255, 255, 0.3);
padding: 2px 8px;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.event-card.has-bg .event-type {
background: rgba(0, 0, 0, 0.4);
.event-type-badge {
font-size: 0.6rem;
padding: 2px 6px;
border-radius: 3px;
background: rgba(0, 0, 0, 0.2);
color: white;
text-shadow: none;
}
.summary {
font-style: italic;
line-height: 1.4;
margin-bottom: 10px;
.status-badge {
font-size: 0.6rem;
padding: 2px 8px;
border-radius: 10px;
color: white;
text-transform: uppercase;
}
.participants-box .label {
font-size: 0.65rem;
.summary-text {
font-size: 0.85rem;
font-style: italic;
margin-bottom: 8px;
line-height: 1.3;
opacity: 0.9;
color: inherit;
}
.mini-label {
font-size: 0.6rem;
text-transform: uppercase;
display: block;
margin-bottom: 5px;
opacity: 0.8;
}
.tags {
.participant-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 4px;
}
.p-tag {
background: rgba(255, 255, 255, 0.2);
font-size: 0.65rem;
background: rgba(0, 0, 0, 0.1);
padding: 1px 5px;
border-radius: 3px;
}
.circle-badge {
background: rgba(255, 255, 255, 0.8);
color: #222;
padding: 2px 6px;
border-radius: 4px;
font-size: 0.65rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.event-card.has-bg .p-tag {
background: rgba(0, 0, 0, 0.4);
text-shadow: none;
}
.event-footer {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.7rem;
padding-top: 10px;
}
.social-impact {
background: #ffffff80;
padding: 2px 8px;
border-radius: 5px;
font-weight: bold;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
font-size: 0.7rem;
}
.event-card.has-bg .social-impact {
background: rgba(0, 0, 0, 0.4);
text-shadow: none;
/* Gestion de l'image de fond si elle existe */
.has-bg {
background-image:
linear-gradient(rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.2)),
var(--bg-image) !important;
background-size: cover;
background-position: center;
}
</style>

View File

@@ -32,13 +32,13 @@ const { title } = Astro.props;
>
<!-- Google tag (gtag.js) -->
<script>
<script is:inline>
// On déclare dataLayer pour que TS ne râle pas
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
window.dataLayer.push(arguments);
}
gtag("js", new Date());
gtag("config", "G-5M920H5PW7");
</script>
@@ -49,49 +49,71 @@ const { title } = Astro.props;
<style is:global>
:root {
--bg-gradient: radial-gradient(
circle at 50% 50%,
#ffffff 0%,
#f1f5f9 100%
);
--card-width: 320px;
--card-height: 480px;
--inner-radius: 10px;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html,
body {
overflow-x: hidden;
height: 100%;
width: 100%;
}
body {
font-family: "Inter", sans-serif;
background-image: var(--bg-gradient);
background-attachment: fixed;
color: #1e293b;
min-height: 100vh;
display: block; /* Change de flex à block pour laisser le contrôle aux pages */
line-height: 1.5;
}
/* Style spécifique pour les titres de cartes TCG */
h1,
h2,
.name,
.trigram {
/* La coquille physique de toutes les cartes */
.tcg-card {
width: var(--card-width);
height: var(--card-height);
border-radius: 18px;
padding: 12px;
font-family: "Philosopher", serif;
position: relative;
box-sizing: border-box;
display: flex;
flex-direction: column;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.15);
color: #1e293b;
}
/* Optimisation mobile pour ton Samsung */
@media (max-width: 480px) {
body {
padding: 10px;
align-items: flex-start; /* Permet de scroller si la carte est longue */
}
/* La bordure intérieure commune */
.card-border {
flex: 1;
border-radius: var(--inner-radius);
border: 1px solid rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
padding: 10px;
overflow: hidden;
background: rgba(
255,
255,
255,
0.1
); /* Transparence pour laisser voir le biome/couleur */
}
/* Les zones partagées par Card et EventCard */
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
font-weight: bold;
margin-bottom: 5px;
}
.card-art {
flex: 1;
margin: 5px 0;
position: relative;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 4px;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.05);
}
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 8px;
margin-top: auto;
}
</style>

View File

@@ -3,26 +3,35 @@ import { getCollection } from "astro:content";
import Layout from "../layouts/Layout.astro";
import Card from "../components/Card.astro";
import EventCard from "../components/EventCard.astro";
import BankCard from "../components/BankCard.astro"; // Import du nouveau composant
const allHumans = await getCollection("humans");
const myHumans = allHumans.filter((entry: any) => entry.id.startsWith("latchimynicolas/"));
const allEvents = await getCollection("events");
const allBanks = await getCollection("banks"); // Récupération des cartes banques/objets
// 1. Tri chronologique global
const sortedEntries = allHumans.sort(
(a, b) => new Date(b.data.date).getTime() - new Date(a.data.date).getTime(),
(a: any, b: any) => new Date(b.data.date).getTime() - new Date(a.data.date).getTime()
);
const sortedEvents = allEvents.sort(
(a, b) => new Date(a.data.date).getTime() - new Date(b.data.date).getTime(),
(a: any, b: any) => new Date(a.data.date).getTime() - new Date(b.data.date).getTime(),
);
// 2. Groupement par utilisateur [cite: 80]
const groupedHumans = sortedEntries.reduce((acc, entry) => {
const sortedBanks = allBanks.sort(
(a: any, b: any) => a.data.name.localeCompare(b.data.name));
// 2. Groupement par utilisateur
const groupedHumans = sortedEntries.reduce((acc: any, entry: any) => {
const userName = entry.data.name;
if (!acc[userName]) acc[userName] = [];
acc[userName].push(entry);
return acc;
}, {});
---
<Layout title="Collection">
@@ -38,27 +47,28 @@ const groupedHumans = sortedEntries.reduce((acc, entry) => {
Object.entries(groupedHumans).map(([userName, cards]) => (
<section class="user-collection">
<h2 class="user-title">
{userName} <span>({cards.length} souffles)</span>
{userName}
<span>({(cards as any[]).length} souffles)</span>
</h2>
<div class="user-flex-row">
{cards.map((entry) => (
<a
href={`/${entry.id.replace(/\.md$/, "")}`}
class="card-link"
>
<div class="card-scaler">
<Card frontmatter={entry.data} />
<div class="entry-meta">
<p>
{new Date(
entry.data.date,
).toLocaleDateString("fr-FR")}
</p>
{
(cards as any[]).map((entry) => (
<a
href={`/${entry.id.replace(/\.md$/, "")}`}
class="card-link"
>
<div class="card-scaler">
<Card frontmatter={entry.data} />
<div class="entry-meta">
<p>
{new Date(entry.data.date).toLocaleDateString("fr-FR")}
</p>
</div>
</div>
</div>
</a>
))}
</a>
))
}
</div>
<hr class="separator" />
</section>
@@ -90,8 +100,28 @@ const groupedHumans = sortedEntries.reduce((acc, entry) => {
</a>
))}
</div>
<hr class="separator" />
<hr class="separator" />
</section>
<section class="user-collection">
<h2 class="user-title">Trésoreries <span>(Sources de Flux)</span></h2>
<div class="user-flex-row">
{
sortedBanks.map((bank) => (
<div class="card-link">
<div class="card-scaler">
{bank.data.sub_category === "finances" ? (
<BankCard frontmatter={bank.data} allHumans={myHumans} />
) : (
/* Autre type de stuff si nécessaire */
<div class="tcg-card">Objet: {bank.data.name}</div>
)}
</div>
</div>
))
}
</div>
</section>
)
}
</main>
@@ -139,6 +169,7 @@ const groupedHumans = sortedEntries.reduce((acc, entry) => {
flex-wrap: wrap;
gap: 45px; /* Gap simple comme demandé */
justify-content: flex-start;
padding: 20px 0;
margin-bottom: 100px;
}

View File

@@ -3,16 +3,20 @@ import { getCollection } from "astro:content";
import Layout from "../layouts/Layout.astro";
import Card from "../components/Card.astro";
import EventCard from "../components/EventCard.astro";
import BankCard from "../components/BankCard.astro"; // <-- AJOUT
// 1. Récupération des données
const allHumans = await getCollection("humans", ({ id }) => {
return id.startsWith("latchimynicolas/");
});
const allEvents = await getCollection("events", ({ data }) => {
// On ne garde que les événements qui ne sont pas terminés
return data.status !== "Terminé";
});
// Récupération de la banque
const allBanks = await getCollection("banks");
const upCard = allBanks.find((b) => b.id === "up" || b.id === "up.md");
// 2. Tris temporels
const sortedAll = allHumans.sort(
(a, b) => new Date(b.data.date).getTime() - new Date(a.data.date).getTime(),
@@ -22,17 +26,18 @@ const latestEvent = allEvents.sort(
(a, b) => new Date(a.data.date).getTime() - new Date(b.data.date).getTime(),
)[0];
// 3. Logique de Streak (Série)
const getStreak = (name, entries) => {
// 3. Logique de Streak (Série) - TON CODE ORIGINAL
const getStreak = (name: any, entries: any) => {
const userDates = entries
.filter((e) => e.data.name === name)
.map((e) => ({
.filter((e: any) => e.data.name === name)
.map((e: any) => ({
date: new Date(e.data.date).toISOString().split("T")[0],
hasHyg:
e.data.manifestations?.some((m) => m.cercle === "HYG") || false,
e.data.manifestations?.some((m: any) => m.cercle === "HYG") ||
false,
}));
const uniqueDates = [...new Set(userDates.map((d) => d.date))]
const uniqueDates = [...new Set(userDates.map((d: any) => d.date))]
.sort()
.reverse();
let streak = 0;
@@ -60,7 +65,6 @@ const getStreak = (name, entries) => {
break;
}
}
const shiftDays = todaySkipped ? 1 : 0;
const last7Days = Array.from({ length: 7 }, (_, j) => {
let d = new Date(
@@ -71,10 +75,9 @@ const getStreak = (name, entries) => {
});
const hygCount = userDates.filter(
(e) => last7Days.includes(e.date) && e.hasHyg,
(e: any) => last7Days.includes(e.date) && e.hasHyg,
).length;
if (hygCount < 3 && streak > 0) streak = Math.max(0, streak - 1);
return streak;
};
@@ -87,19 +90,6 @@ const latestPerUser = Array.from(
}, new Map())
.values(),
);
// 5. CALCUL DE SYNERGIE
const activeSynergies = latestPerUser.map((user) => {
const matchingManifestation = user.data.manifestations?.find(
(m) => m.cercle === latestEvent?.data.circle,
);
return {
userName: user.data.name,
hasSynergy: !!matchingManifestation,
cercle: latestEvent?.data.circle,
type: matchingManifestation?.type,
};
});
---
<Layout title="Dashboard & Codex">
@@ -113,74 +103,7 @@ const activeSynergies = latestPerUser.map((user) => {
<main>
<h1 class="main-title">Ambient</h1>
<section class="design-intent">
<header class="rules-header">
<h2>🛠️ Chantier Ergonomique (en cours)</h2>
<p class="intent-quote">
<em
>"Repenser la visualisation des cartes pour un parcours
utilisateur en totale concordance avec les besoins du
type d'utilisateur."</em
>
</p>
</header>
<div class="work-todo-grid">
<div class="todo-column">
<h3>🎴 Ergonomie des Cartes</h3>
<ul>
<li>
Optimiser le ratio d'aspect pour une lecture
mobile-first sur Samsung/Android.
</li>
<li>
Intégrer les indicateurs de "Synergie" directement
sur le visuel de la carte.
</li>
<li>
Refondre le système de rareté (Shiny/Légendaire)
avec des effets CSS avancés.
</li>
</ul>
</div>
<div class="todo-column">
<h3>🛤️ Parcours Utilisateur</h3>
<ul>
<li>
Simplifier la transition entre la vue "Dashboard" et
la "Collection" complète.
</li>
<li>
Créer des points d'entrée contextuels selon le
cercle dominant (PRO, SAN, etc.).
</li>
<li>
Améliorer la narration visuelle entre le manuel de
jeu et les logs d'événements.
</li>
</ul>
</div>
<div class="todo-column">
<h3>⚙️ Technique & Backlog</h3>
<ul>
<li>
Automatiser la mise à jour des versions (Changelog)
via les fichiers Markdown.
</li>
<li>
Améliorer la performance du rendu Astro sur le
serveur Raspberry Pi 4.
</li>
<li>
Finaliser les composants EventCard pour les
correctifs (Fixes).
</li>
</ul>
</div>
</div>
</section>
<section class="hero-flex">
{
latestPerUser.map((entry) => {
@@ -214,162 +137,37 @@ const activeSynergies = latestPerUser.map((user) => {
</div>
)
}
</section>
<hr class="separator" />
<section class="system-rules">
<header class="rules-header">
<h2>📖 Manuel de Jeu : TCG (Ruleset Complet)</h2>
<p>Traduction technique des algorithmes du moteur de jeu</p>
</header>
<div class="rules-layout">
<div class="rule-category">
<h3>✨ Rareté & Évolution</h3>
<ul>
<li>
<strong>SHINY :</strong> Streak &ge; 3 + 3 cercles uniques.
</li>
<li>
<strong>LÉGENDAIRE :</strong> Streak &ge; 10 + Harmonie
&ge; 4.
</li>
<li>
<strong>MYTHIQUE :</strong> Streak &ge; 20 + 0 Hallucinations.
</li>
</ul>
</div>
<div class="rule-category">
<h3>⚔️ Système de Combat</h3>
<ul>
<li>
<strong>Puissance (ATK) :</strong>
<code
>(PRO × 2) + (Motiv/4) + (Énergie/3) + Bonus
Harmonie + StreakBonus</code
>.
<em
>Le travail (PRO) est ton multiplicateur
principal.</em
>
</li>
<li>
<strong>Résilience (DEF) :</strong>
<code
>10 + (Hygiène/2) - Stress - Hallu - Pénalité
Sommeil + StreakBonus</code
>.
<em
>L'hygiène est ton armure, le stress ta faille.</em
>
</li>
<li>
<strong>Coût d'Invocation :</strong> Nombre total de manifestations
divisé par 2 (arrondi au supérieur).
</li>
</ul>
</div>
<div class="rule-category">
<h3>📈 Séries & Rareté (Streak)</h3>
<ul>
<li>
<strong>Streak Bonus :</strong> +1 ATK/DEF tous les 7
jours de série consécutifs.
</li>
<li>
<strong>Éveil Shiny :</strong> Activé dès 3 jours de série
+ 3 cercles différents activés.
</li>
<li>
<strong>Échelle de Rareté :</strong>
<br />• <strong>PEU COMMUNE :</strong> 3 jours de série.
<br />• <strong>RARE :</strong> 6 jours de série.
<br />• <strong>LÉGENDAIRE :</strong> 10 jours + Harmonie
&ge; 4.
<br />• <strong>MYTHIQUE :</strong> 20 jours + 0 Hallucinations.
</li>
</ul>
</div>
<div class="rule-category">
<h3>🌍 Influence des Biomes</h3>
<ul>
<li>
<strong>ÉTANG (L'Inerte) :</strong> Priorité Somatique.
S'active si <code>Stress &ge; 7</code>, <code
>Hallu &ge; 2</code
> ou <code>Harmonie &le; -3</code>.
</li>
<li>
<strong>BELOUVE (La Racine) :</strong> État d'Harmonie.
S'active si 3+ cercles sont présents avec au moins un
log <strong>SAN</strong>.
</li>
<li>
<strong>OCEAN (Le Flux) :</strong> S'active si le Social
(SOC) domine le Pro ou si le Flux (FLX) est &gt; 2.
</li>
<li>
<strong>FOURNAISE (La Forge) :</strong> S'active par dominance
technique (PRO &ge; 2 et PRO &ge; SOC).
</li>
<li>
<strong>SAVANE (La Clarté) :</strong> Biome d'équilibre
par défaut.
</li>
</ul>
</div>
<div class="rule-category">
<h3>🔧 Maintenance & Malus</h3>
<ul>
<li>
<strong>Pénalité Sommeil :</strong> Si (Sommeil - Malus
Travail) &lt; 6h, ta DEF baisse de 2.
<em>(Le malus travail s'active si PRO &ge; 5).</em>
</li>
<li>
<strong>Harmonie des Cercles :</strong> Posséder les 5
cercles (SAN, FLX, PRO, SOC, HYG) sur un log octroie un
bonus massif de **+2** au Score d'Harmonie.
</li>
<li>
<strong>Règle HYG (Prévention) :</strong> Si moins de
3 logs "HYG" sont détectés sur les 7 derniers jours, la
série (Streak) est amputée d'un point de pénalité.
</li>
</ul>
</div>
<div class="rule-category">
<h3>🎭 Objets & Équipements</h3>
<ul>
<li>
<strong>VÉLO :</strong> Convertit 2 points de Stress en
1 point d'Énergie au petit matin.
</li>
<li>
<strong>ARIPIPRAZOLE :</strong> Fixe Hallucinations à
0. Ajoute le trait "Stabilité".
</li>
</ul>
</div>
</div>
{
upCard && (
<div class="card-focus">
<BankCard
frontmatter={upCard.data}
allHumans={allHumans}
/>
<div class="entry-meta">
<p>État du cercle FLX</p>
</div>
</div>
)
}
</section>
</main>
</Layout>
<style>
.design-intent {
max-width: 1400px; /* Occupation maximale de l'espace */
margin: 60px auto 100px auto;
padding: 40px;
background: #f8fafc;
border-radius: 24px;
border: 1px dashed #cbd5e1; /* Aspect "travail en cours" */
.hero-flex {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 40px;
padding: 60px 20px;
scroll-snap-type: x mandatory; /* Optionnel pour le swipe Samsung */
}
.card-focus {
scroll-snap-align: center;
}
/* ... le reste de ton CSS ... */
.intent-quote {
max-width: 800px;