mirror of
https://github.com/IliesAllali/parenthese.git
synced 2026-11-06 23:32:08 +00:00
Merge pull request #178 from IliesAllali/feat/squelettes
Blocs gris pendant le chargement (onglet Vie, panneaux de l'arbre)
This commit is contained in:
9 files changed
+63
-5
No files matched your search
@@ -24,6 +24,7 @@ import { removeManager } from '../api/managersApi.js'
|
||||
import { t } from '../i18n/index.js'
|
||||
import { readGedcomFile } from '../utils/gedcomText.js'
|
||||
import { readGedcomBundle, zipErrorMessage } from '../utils/gedcomBundle.js'
|
||||
import Skeleton from './Skeleton.jsx'
|
||||
|
||||
function normalizeSlug(value) {
|
||||
return String(value || '')
|
||||
@@ -442,7 +443,7 @@ const AdminPanel = ({
|
||||
|
||||
<div className="ts-content">
|
||||
{loading ? (
|
||||
<p className="pz-small ts-loading">{t('Chargement…')}</p>
|
||||
<Skeleton variant="lines" count={4} />
|
||||
) : (
|
||||
<>
|
||||
{errorMessage && <div className="pz-error" role="alert">{errorMessage}</div>}
|
||||
|
||||
@@ -8,6 +8,7 @@ import PzBusy from './PzBusy.jsx'
|
||||
import { HELP_URL } from '../utils/siteLinks.js'
|
||||
import { t, dateLocale } from '../i18n/index.js'
|
||||
import { sexLabel } from '../utils/personSex.js'
|
||||
import Skeleton from './Skeleton.jsx'
|
||||
|
||||
// ─── helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -471,7 +472,7 @@ const ContributionPanel = ({
|
||||
{errorMessage && <div className="contrib-error">{errorMessage}</div>}
|
||||
|
||||
{loading ? (
|
||||
<div className="contrib-empty">{t('Chargement…')}</div>
|
||||
<Skeleton variant="rows" count={3} />
|
||||
) : sessions.length === 0 ? (
|
||||
<div className="contrib-empty">{t("Rien à relire pour l'instant. Les propositions de la famille apparaîtront ici.")}</div>
|
||||
) : (
|
||||
|
||||
@@ -4,6 +4,7 @@ import { t } from '../i18n/index.js'
|
||||
import { buildLifeTimeline } from '../utils/lifeTimeline.js'
|
||||
import './LifeTimeline.css'
|
||||
import { blurPlaceholder } from '../utils/blurhash.js'
|
||||
import Skeleton from './Skeleton.jsx'
|
||||
|
||||
// Onglet Vie de la fiche : naissance, unions et décès en grands repères, les autres événements
|
||||
// plus petits entre eux, la famille proche en retrait. Les sources restent repliées.
|
||||
@@ -71,6 +72,9 @@ export default function LifeTimeline({
|
||||
const noticeLine = notice && <p className="pz-success lt-notice" role="status">{notice}</p>
|
||||
|
||||
if (entries.length === 0) {
|
||||
// Premier chargement, aucune date connue sur la fiche : la place de la frise, au lieu d'un « pas encore de
|
||||
// date » qui serait faux une seconde après. Avec des dates connues, elles s'affichent tout de suite.
|
||||
if (loading && !life) return <Skeleton variant="timeline" count={3} label={t('Chargement de la frise')} />
|
||||
return (
|
||||
<div className="pc-empty">
|
||||
<p className="pz-sub">{t('Pas encore de date connue pour {name}.', { name: person.firstName })}</p>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { fetchSearchSources, searchSource } from '../api/searchSourcesApi.js'
|
||||
import { SEARCH_LINKS, SEARCH_LINK_REGIONS } from '../data/searchLinks.js'
|
||||
import { changesFromResult, personFromResult } from '../utils/searchResultChanges.js'
|
||||
import { fullNameLabel } from '../utils/personName.js'
|
||||
import Skeleton from './Skeleton.jsx'
|
||||
|
||||
// Ce que chaque source interrogée couvre et ce qui part vers elle : affiché avant qu'on coche
|
||||
const SOURCE_INFO = {
|
||||
@@ -283,7 +284,7 @@ function SearchSourcesPanel({ visible, treeId, token, persons = [], canApply = f
|
||||
|
||||
<div className="apn-section sp-sources">
|
||||
<p className="pz-label">{t('Où chercher')}</p>
|
||||
{sources === null && <p className="pz-hint">{t('Chargement…')}</p>}
|
||||
{sources === null && <Skeleton variant="lines" count={3} />}
|
||||
{sources?.length === 0 && (
|
||||
<p className="pz-hint">{t("Ce serveur n'interroge aucune source : aucun nom ne quitte l'instance. Les liens plus bas ouvrent les sites dans votre navigateur.")}</p>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/* Blocs gris de chargement (Skeleton.jsx) : immobiles, couleur des séparateurs */
|
||||
.pz-skel { display: grid; gap: 14px; padding: 4px 0; }
|
||||
.pz-skel-row { display: flex; align-items: center; gap: 12px; }
|
||||
.pz-skel-text { display: grid; gap: 7px; flex: 1; min-width: 0; }
|
||||
.pz-skel-bar { display: block; height: 12px; border-radius: 6px; background: var(--pz-line); }
|
||||
.pz-skel-bar--thin { height: 9px; background: var(--pz-line-soft); }
|
||||
.pz-skel-dot { flex: none; width: 32px; height: 32px; border-radius: 50%; background: var(--pz-line); }
|
||||
|
||||
/* Frise : année, point sur le rail, texte */
|
||||
.pz-skel--timeline { gap: 22px; padding: 8px 0 4px; }
|
||||
.pz-skel--timeline .pz-skel-year { flex: none; width: 34px; height: 11px; }
|
||||
.pz-skel--timeline .pz-skel-dot { width: 10px; height: 10px; }
|
||||
|
||||
.pz-skel--lines .pz-skel-bar { height: 10px; }
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { t } from '../i18n/index.js'
|
||||
import './Skeleton.css'
|
||||
|
||||
// Blocs gris à la forme de ce qui arrive, à la place d'un « Chargement… ». Rien avant SHOW_AFTER : une réponse
|
||||
// rapide ne fait pas clignoter l'écran. Aucun mouvement (pas de reflet qui passe), juste la place réservée.
|
||||
const SHOW_AFTER = 300
|
||||
|
||||
// variant : rows (avatar et deux lignes : personnes, visites, propositions), timeline (frise de l'onglet Vie),
|
||||
// lines (paragraphes)
|
||||
export default function Skeleton({ variant = 'rows', count = 3, label = '' }) {
|
||||
const [shown, setShown] = useState(false)
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setShown(true), SHOW_AFTER)
|
||||
return () => clearTimeout(timer)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={`pz-skel pz-skel--${variant}`} role="status" aria-busy="true">
|
||||
<span className="pz-sr">{label || t('Chargement…')}</span>
|
||||
{shown && Array.from({ length: count }, (_, i) => (
|
||||
<div key={i} className="pz-skel-row" aria-hidden="true">
|
||||
{variant === 'rows' && <span className="pz-skel-dot" />}
|
||||
{variant === 'timeline' && <span className="pz-skel-bar pz-skel-year" />}
|
||||
{variant === 'timeline' && <span className="pz-skel-dot" />}
|
||||
<span className="pz-skel-text">
|
||||
<span className="pz-skel-bar" style={{ width: `${[62, 48, 70, 55][i % 4]}%` }} />
|
||||
{variant !== 'lines' && <span className="pz-skel-bar pz-skel-bar--thin" style={{ width: `${[38, 30, 44, 34][i % 4]}%` }} />}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { inviteManager, invitationUrl, listManagers, removeManager, renewInvitat
|
||||
import PzBusy from './PzBusy.jsx'
|
||||
import { dateLocale, t } from '../i18n/index.js'
|
||||
import { initialOf } from '../utils/initial.js'
|
||||
import Skeleton from './Skeleton.jsx'
|
||||
|
||||
// Onglet Gestion des paramètres de l'arbre (issue #145). Le propriétaire invite des co-gestionnaires par leur
|
||||
// adresse : Parenthèse n'envoie pas de mail, il donne un lien à transmettre. Les co-gestionnaires voient la liste.
|
||||
@@ -102,7 +103,7 @@ const TreeManagersSection = ({ treeId, authToken, currentUserId = null, oidcName
|
||||
}
|
||||
}
|
||||
|
||||
if (state.loading) return <p className="pz-small ts-loading">{t('Chargement…')}</p>
|
||||
if (state.loading) return <Skeleton variant="rows" count={2} />
|
||||
if (state.error) return <div className="pz-error" role="alert">{state.error}</div>
|
||||
|
||||
const { canManage, managers, invitations } = state.data
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { listTreeVisits } from '../api/treeApi'
|
||||
import { dateLocale, t } from '../i18n/index.js'
|
||||
import Skeleton from './Skeleton.jsx'
|
||||
|
||||
// Onglet Visites des paramètres de l'arbre : qui est venu, combien de fois.
|
||||
// Le journal est tenu par le serveur à chaque ouverture de l'arbre (backend/src/lib/tree-visits.ts).
|
||||
@@ -37,7 +38,7 @@ const TreeVisitsSection = ({ treeId, authToken }) => {
|
||||
return () => { active = false }
|
||||
}, [treeId, authToken])
|
||||
|
||||
if (state.loading) return <p className="pz-small ts-loading">{t('Chargement…')}</p>
|
||||
if (state.loading) return <Skeleton variant="rows" count={4} />
|
||||
if (state.error) return <div className="pz-error" role="alert">{state.error}</div>
|
||||
|
||||
const { summary, recent } = state.data
|
||||
|
||||
@@ -432,6 +432,7 @@ export default {
|
||||
'Naissance de {name}': 'Birth of {name}',
|
||||
'Décès à {place}': 'Died in {place}',
|
||||
'Ajouter un moment': 'Add a moment',
|
||||
'Chargement de la frise': 'Loading the timeline',
|
||||
'Modifier « {title} »': 'Edit “{title}”',
|
||||
'Déménagement': 'Move',
|
||||
'L’enregistrement n’a pas abouti. Réessayez dans un instant.': 'Saving did not go through. Try again in a moment.',
|
||||
|
||||
Reference in new issue
Block a user