Author SHA1 Message Date
cedric 99daeb014c feat: styliser le lien vers le fil BOKANTE
Déploiement FRONT BETA / check (push) Successful in 2m7s
Déploiement FRONT PROD / check (push) Successful in 2m15s
Déploiement FRONT BETA / deploy (push) Successful in 24s
Déploiement FRONT PROD / deploy (push) Successful in 24s
2026-07-07 16:13:57 +04:00
cedric f5a4014e41 feat: ajouter un sélecteur d'instance Mastodon et expliciter BOKANTE 2026-07-07 16:11:46 +04:00
cedric d66c679fd1 revert: ne plus présélectionner selon la langue du navigateur
Déploiement FRONT BETA / check (push) Successful in 2m11s
Déploiement FRONT PROD / check (push) Successful in 2m11s
Déploiement FRONT BETA / deploy (push) Successful in 21s
Déploiement FRONT PROD / deploy (push) Successful in 20s
2026-07-06 02:34:08 +04:00
cedric cab293d62e Merge pull request 'Sélecteur de langue : regroupement par continent, Afrique en tête' (#6) from feat/africa into master
Déploiement FRONT BETA / check (push) Successful in 2m11s
Déploiement FRONT PROD / check (push) Successful in 2m16s
Déploiement FRONT BETA / deploy (push) Successful in 22s
Déploiement FRONT PROD / deploy (push) Successful in 19s
Reviewed-on: #6
2026-07-05 22:20:46 +00:00
cedric 5936de59ff fix: mettre en valeur l'option Transcription dans le select
Déploiement FRONT BETA / check (push) Successful in 2m7s
Vérification PR / check (pull_request) Successful in 2m11s
Déploiement FRONT BETA / deploy (push) Successful in 20s
Vérification PR / deploy-beta (pull_request) Successful in 19s
2026-07-06 02:01:44 +04:00
cedric 6804e3626f feat: remplacer les puces par un select groupé par continent
Déploiement FRONT BETA / check (push) Successful in 2m14s
Vérification PR / check (pull_request) Successful in 2m14s
Déploiement FRONT BETA / deploy (push) Successful in 23s
Vérification PR / deploy-beta (pull_request) Successful in 20s
2026-07-06 00:18:34 +04:00
cedric 461b063cff feat: regrouper les langues par continent, Afrique en tête 2026-07-06 00:10:34 +04:00
cedric 6601b00545 fix: ne plus exposer le jeton API au navigateur (recherche d'artiste)
Déploiement FRONT PROD / check (push) Successful in 2m13s
Déploiement FRONT PROD / deploy (push) Successful in 20s
Déploiement FRONT BETA / check (push) Successful in 2m6s
Déploiement FRONT BETA / deploy (push) Successful in 20s
2026-07-05 23:16:19 +04:00
cedric f88b0184ea feat: sélectionner la traduction par défaut selon la langue du navigateur
Déploiement FRONT BETA / check (push) Successful in 2m12s
Déploiement FRONT PROD / check (push) Successful in 2m14s
Déploiement FRONT BETA / deploy (push) Successful in 21s
Déploiement FRONT PROD / deploy (push) Successful in 20s
2026-07-05 22:33:09 +04:00
cedric 43964ec91d feat: afficher les langues asiatiques avant les langues européennes 2026-07-05 22:28:57 +04:00
cedric ce19fa3b5d fix: remplacer le libellé de date par une icône universelle
Déploiement FRONT BETA / check (push) Successful in 2m10s
Déploiement FRONT PROD / check (push) Successful in 2m5s
Déploiement FRONT BETA / deploy (push) Successful in 20s
Déploiement FRONT PROD / deploy (push) Successful in 19s
2026-07-05 20:54:14 +04:00
cedric 33514bf633 feat: généraliser le formatage de date adaptatif à la locale 2026-07-05 20:47:52 +04:00
cedric 35c39a97f3 feat: adapter la date des commentaires à la locale du navigateur
Déploiement FRONT PROD / deploy (push) Successful in 20s
Déploiement FRONT BETA / check (push) Successful in 2m12s
Déploiement FRONT PROD / check (push) Successful in 2m11s
Déploiement FRONT BETA / deploy (push) Successful in 21s
2026-07-05 20:39:50 +04:00
cedric 72b86fdc5c feat: mettre en valeur les mentions et afficher l'heure des commentaires
Déploiement FRONT PROD / deploy (push) Successful in 19s
Déploiement FRONT BETA / check (push) Successful in 2m7s
Déploiement FRONT PROD / check (push) Successful in 2m9s
Déploiement FRONT BETA / deploy (push) Successful in 21s
2026-07-05 20:17:35 +04:00
cedric 53287f9c80 feat: remplacer les onglets de langue par des puces adaptatives
Déploiement FRONT PROD / check (push) Successful in 2m16s
Déploiement FRONT BETA / check (push) Successful in 2m11s
Déploiement FRONT BETA / deploy (push) Successful in 21s
Déploiement FRONT PROD / deploy (push) Successful in 21s
2026-07-05 20:09:29 +04:00
cedric 8979970a90 feat: refonte visuelle des commentaires (cartes, compteur, date)
Déploiement FRONT BETA / check (push) Successful in 2m18s
Déploiement FRONT PROD / check (push) Successful in 2m7s
Déploiement FRONT BETA / deploy (push) Successful in 21s
Déploiement FRONT PROD / deploy (push) Successful in 19s
2026-07-05 19:50:54 +04:00
cedric 75c5979478 feat: ajouter le logo Mastodon au badge des commentaires fédérés
Déploiement FRONT BETA / check (push) Successful in 2m12s
Déploiement FRONT PROD / check (push) Successful in 2m16s
Déploiement FRONT BETA / deploy (push) Successful in 23s
Déploiement FRONT PROD / deploy (push) Successful in 22s
2026-07-05 19:35:40 +04:00
cedric 5778d865e2 docs: documenter les commentaires fédérés (bokante)
Déploiement FRONT BETA / check (push) Successful in 2m7s
Déploiement FRONT PROD / check (push) Successful in 2m11s
Déploiement FRONT BETA / deploy (push) Successful in 20s
Déploiement FRONT PROD / deploy (push) Successful in 21s
2026-07-05 02:07:03 +04:00
cedric 0ac18faf5c Merge pull request 'Commentaires fédérés via Mastodon (bokante.o-k-i.net) — frontend' (#5) from feat/comment-fedi into master
Déploiement FRONT BETA / deploy (push) Successful in 21s
Déploiement FRONT PROD / deploy (push) Successful in 29s
Déploiement FRONT BETA / check (push) Successful in 2m5s
Déploiement FRONT PROD / check (push) Successful in 2m12s
Reviewed-on: #5
2026-07-04 21:56:48 +00:00
13 changed files with 357 additions and 117 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ API_URL=http://localhost:1337/api
NEXT_PUBLIC_API_URL=$API_URL NEXT_PUBLIC_API_URL=$API_URL
NEXT_PUBLIC_API_URL_ROOT=http://localhost:1337 NEXT_PUBLIC_API_URL_ROOT=http://localhost:1337
SITE_URL=http://localhost:3001 SITE_URL=http://localhost:3001
NEXT_PUBLIC_READ_TOKEN= API_READ_TOKEN=
# IDENTITÉ DU SITE (branding) # IDENTITÉ DU SITE (branding)
NEXT_PUBLIC_SITE_NAME=PAWÒL-NU. Paroles et traductions. NEXT_PUBLIC_SITE_NAME=PAWÒL-NU. Paroles et traductions.
+18
View File
@@ -24,6 +24,24 @@ cp .env.sample .env
yarn && yarn dev yarn && yarn dev
``` ```
## Commentaires fédérés (Mastodon / bokante.o-k-i.net)
Les commentaires sous une parole peuvent provenir d'un compte pawol.nu ou du Fediverse
(réponses reçues sur le statut miroir publié sur bokante.o-k-i.net par le backend). Un
bouton permet de répondre directement depuis son propre compte Mastodon, quelle que soit
son instance, via `authorize_interaction` — aucun compte pawol.nu requis.
**Variables d'environnement :**
| Variable | Description |
|---|---|
| `NEXT_PUBLIC_BOKANTE_URL` | Instance Mastodon de l'organisation (défaut : `https://bokante.o-k-i.net`) |
| `NEXT_PUBLIC_BOKANTE_ACCOUNT` | Compte bot dont les statuts servent de miroir (défaut : `pawol_nu`) |
Voir le RFC dédié pour le détail de la conception
(`RFC-commentaires-activitypub-2026-07-04.md`, à la racine du dossier `PAWOL.NU`) et le
`README.md` de `api.pawol.nu` pour la configuration côté backend.
## License ## License
Copyright (C) 2020 - 2026 Cédric Famibelle-Pronzola & ORGANISATION KA INTERNATIONALE (OKI) Copyright (C) 2020 - 2026 Cédric Famibelle-Pronzola & ORGANISATION KA INTERNATIONALE (OKI)
+23
View File
@@ -0,0 +1,23 @@
import {describe, it, expect, vi, afterEach} from 'vitest'
import {jwennToutAwtis} from '../../../../lib/oki-api'
import {GET} from '../route'
vi.mock('../../../../lib/oki-api', () => ({
jwennToutAwtis: vi.fn()
}))
describe('GET /api/awtis', () => {
afterEach(() => {
vi.restoreAllMocks()
})
it('renvoie les données de jwennToutAwtis en JSON, sans exposer de jeton', async () => {
jwennToutAwtis.mockResolvedValue({data: [{id: 1, alias: 'Foo'}], meta: {}})
const response = await GET()
const body = await response.json()
expect(body).toEqual({data: [{id: 1, alias: 'Foo'}], meta: {}})
expect(JSON.stringify(body)).not.toMatch(/bearer|token/i)
})
})
+6
View File
@@ -0,0 +1,6 @@
import {jwennToutAwtis} from '../../../lib/oki-api'
export async function GET() {
const data = await jwennToutAwtis()
return Response.json(data)
}
+1 -2
View File
@@ -7,7 +7,6 @@ import Autocomplete from '@mui/material/Autocomplete'
import Avatar from '@mui/material/Avatar' import Avatar from '@mui/material/Avatar'
import Container from '@mui/material/Container' import Container from '@mui/material/Container'
import {jwennToutAwtis} from '../../lib/oki-api'
import {formatKuveti} from '../../lib/kuveti' import {formatKuveti} from '../../lib/kuveti'
const IMAGE_URL = process.env.NEXT_PUBLIC_API_URL_ROOT || 'http://localhost:1337' const IMAGE_URL = process.env.NEXT_PUBLIC_API_URL_ROOT || 'http://localhost:1337'
@@ -27,7 +26,7 @@ export default function ChecheAwtis() {
(async () => { (async () => {
try { try {
const {data} = await jwennToutAwtis() const {data} = await fetch('/api/awtis').then(response => response.json())
const filteredData = data.map(artiste => { const filteredData = data.map(artiste => {
const firstLetter = artiste.alias[0].toUpperCase() const firstLetter = artiste.alias[0].toUpperCase()
@@ -34,4 +34,18 @@ describe('Komante', () => {
expect(screen.getByRole('link', {name: 'Quelqu\'un'})).toHaveAttribute('href', 'https://mastodon.social/@quelqun') expect(screen.getByRole('link', {name: 'Quelqu\'un'})).toHaveAttribute('href', 'https://mastodon.social/@quelqun')
expect(screen.getByRole('link', {name: /voir sur mastodon/i})).toHaveAttribute('href', 'https://bokante.o-k-i.net/@quelqun/1') expect(screen.getByRole('link', {name: /voir sur mastodon/i})).toHaveAttribute('href', 'https://bokante.o-k-i.net/@quelqun/1')
}) })
it('met en valeur les mentions @ dans le contenu', () => {
render(<Komante commentaires={[{
id: 3,
contenu: '@pawol_nu oh oh ye ye, avec @cybermawonaj@bokante.o-k-i.net aussi',
origine: 'activitypub',
auteurNom: 'Quelqu\'un'
}]} />)
const mansyon = screen.getByText('@pawol_nu')
expect(mansyon).toBeInTheDocument()
expect(screen.getByText('@cybermawonaj@bokante.o-k-i.net')).toBeInTheDocument()
expect(screen.getByText(/oh oh ye ye/)).toBeInTheDocument()
})
}) })
@@ -15,19 +15,36 @@ describe('ReponsMastodon', () => {
expect(container).toBeEmptyDOMElement() expect(container).toBeEmptyDOMElement()
}) })
it('affiche un lien direct vers le fil bokante', () => { it('affiche un lien direct vers le fil BOKANTE', () => {
render(<ReponsMastodon bokanteStatusId='112233' />) render(<ReponsMastodon bokanteStatusId='112233' />)
const lien = screen.getByRole('link', {name: 'Mastodon'}) const lien = screen.getByRole('link', {name: 'BOKANTE (Mastodon)'})
expect(lien).toHaveAttribute('href', 'https://bokante.o-k-i.net/@pawol_nu/112233') expect(lien).toHaveAttribute('href', 'https://bokante.o-k-i.net/@pawol_nu/112233')
}) })
it('redirige vers authorize_interaction avec le domaine extrait d\'un identifiant complet', async () => { it('redirige vers BOKANTE par défaut quand on continue', async () => {
window.open = vi.fn() window.open = vi.fn()
const user = userEvent.setup() const user = userEvent.setup()
render(<ReponsMastodon bokanteStatusId='112233' />) render(<ReponsMastodon bokanteStatusId='112233' />)
await user.click(screen.getByRole('button', {name: /répondre via mastodon/i})) await user.click(screen.getByRole('button', {name: /répondre sur mastodon/i}))
await user.type(screen.getByLabelText(/@vous@votre-instance/i), '@quelqun@mastodon.social') await user.click(screen.getByRole('button', {name: /continuer/i}))
expect(window.open).toHaveBeenCalledWith(
'https://bokante.o-k-i.net/authorize_interaction?uri=' + encodeURIComponent('https://bokante.o-k-i.net/@pawol_nu/112233'),
'_blank',
'noopener,noreferrer'
)
})
it('permet de choisir une autre instance dans le select', async () => {
window.open = vi.fn()
const user = userEvent.setup()
render(<ReponsMastodon bokanteStatusId='112233' />)
await user.click(screen.getByRole('button', {name: /répondre sur mastodon/i}))
await user.click(screen.getByLabelText(/instance mastodon/i))
await user.click(screen.getByRole('option', {name: /autre instance/i}))
await user.type(screen.getByLabelText(/@vous@votre-instance/i), 'mastodon.social')
await user.click(screen.getByRole('button', {name: /continuer/i})) await user.click(screen.getByRole('button', {name: /continuer/i}))
expect(window.open).toHaveBeenCalledWith( expect(window.open).toHaveBeenCalledWith(
@@ -37,17 +54,19 @@ describe('ReponsMastodon', () => {
) )
}) })
it('accepte aussi juste un nom d\'instance sans @', async () => { it('accepte un identifiant complet pour une autre instance', async () => {
window.open = vi.fn() window.open = vi.fn()
const user = userEvent.setup() const user = userEvent.setup()
render(<ReponsMastodon bokanteStatusId='112233' />) render(<ReponsMastodon bokanteStatusId='112233' />)
await user.click(screen.getByRole('button', {name: /répondre via mastodon/i})) await user.click(screen.getByRole('button', {name: /répondre sur mastodon/i}))
await user.type(screen.getByLabelText(/@vous@votre-instance/i), 'mastodon.social') await user.click(screen.getByLabelText(/instance mastodon/i))
await user.click(screen.getByRole('option', {name: /autre instance/i}))
await user.type(screen.getByLabelText(/@vous@votre-instance/i), '@quelqun@mastodon.social')
await user.click(screen.getByRole('button', {name: /continuer/i})) await user.click(screen.getByRole('button', {name: /continuer/i}))
expect(window.open).toHaveBeenCalledWith( expect(window.open).toHaveBeenCalledWith(
expect.stringContaining('https://mastodon.social/authorize_interaction'), 'https://mastodon.social/authorize_interaction?uri=' + encodeURIComponent('https://bokante.o-k-i.net/@pawol_nu/112233'),
'_blank', '_blank',
'noopener,noreferrer' 'noopener,noreferrer'
) )
+1 -4
View File
@@ -1,9 +1,6 @@
import React, {forwardRef, useState} from 'react' import React, {forwardRef, useState} from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import {format} from 'date-fns'
import {fr} from 'date-fns/locale'
import Button from '@mui/material/Button' import Button from '@mui/material/Button'
import Dialog from '@mui/material/Dialog' import Dialog from '@mui/material/Dialog'
import DialogTitle from '@mui/material/DialogTitle' import DialogTitle from '@mui/material/DialogTitle'
@@ -50,7 +47,7 @@ export default function DiferansDialog({difference}) {
<List sx={{width: '100%', maxWidth: 360, bgcolor: 'background.paper'}}> <List sx={{width: '100%', maxWidth: 360, bgcolor: 'background.paper'}}>
{difference.map(({id, admin_user, date, jsonDiff}) => { {difference.map(({id, admin_user, date, jsonDiff}) => {
const {firstname} = admin_user const {firstname} = admin_user
const diferansDate = format(new Date(date), 'PPPppp', {locale: fr}) const diferansDate = new Intl.DateTimeFormat(undefined, {dateStyle: 'full', timeStyle: 'medium'}).format(new Date(date))
return ( return (
<React.Fragment key={id}> <React.Fragment key={id}>
+75 -25
View File
@@ -2,10 +2,12 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import Box from '@mui/material/Box' import Box from '@mui/material/Box'
import Stack from '@mui/material/Stack'
import Avatar from '@mui/material/Avatar' import Avatar from '@mui/material/Avatar'
import Typography from '@mui/material/Typography' import Typography from '@mui/material/Typography'
import Chip from '@mui/material/Chip' import Chip from '@mui/material/Chip'
import Link from 'next/link' import Link from 'next/link'
import {Mastodon} from '@icons-pack/react-simple-icons'
function formatAuteur(commentaire) { function formatAuteur(commentaire) {
if (commentaire.origine === 'activitypub') { if (commentaire.origine === 'activitypub') {
@@ -15,6 +17,29 @@ function formatAuteur(commentaire) {
return commentaire.user?.username || 'Anonim' return commentaire.user?.username || 'Anonim'
} }
function formatDat(commentaire) {
if (!commentaire.datePublication) {
return null
}
return new Intl.DateTimeFormat(undefined, {dateStyle: 'short', timeStyle: 'short'})
.format(new Date(commentaire.datePublication))
}
const MENTION_REGEX = /(?:@[\w.-]+){1,2}/g
function metanValèMansyon(contenu) {
const mansyon = contenu.match(MENTION_REGEX) || []
return contenu.split(MENTION_REGEX).flatMap((moso, index) => (
mansyon[index] ? [
moso,
<Box key={index} component='span' sx={{color: 'primary.main', fontWeight: 700}}>
{mansyon[index]}
</Box>
] : [moso]
))
}
export default function Komante({commentaires}) { export default function Komante({commentaires}) {
if (!commentaires || commentaires.length === 0) { if (!commentaires || commentaires.length === 0) {
return null return null
@@ -23,36 +48,61 @@ export default function Komante({commentaires}) {
return ( return (
<Box sx={{maxWidth: 700, margin: '2em auto 0'}}> <Box sx={{maxWidth: 700, margin: '2em auto 0'}}>
<Typography gutterBottom variant='h6'> <Typography gutterBottom variant='h6'>
Komantè Komantè ({commentaires.length})
</Typography> </Typography>
{commentaires.map(commentaire => { <Stack spacing={1.5}>
const auteur = formatAuteur(commentaire) {commentaires.map(commentaire => {
const estFedere = commentaire.origine === 'activitypub' const auteur = formatAuteur(commentaire)
const estFedere = commentaire.origine === 'activitypub'
const dat = formatDat(commentaire)
return ( return (
<Box key={commentaire.id} sx={{display: 'flex', gap: 1.5, mb: 2}}> <Box
<Avatar src={commentaire.auteurAvatarUrl} alt={auteur} /> key={commentaire.id}
<Box sx={{flex: 1}}> sx={{
<Typography variant='subtitle2' component='div'> display: 'flex',
{estFedere && commentaire.auteurProfilUrl ? ( gap: 1.5,
<Link href={commentaire.auteurProfilUrl} target='_blank' rel='noopener noreferrer'> p: 2,
{auteur} borderRadius: 2,
bgcolor: 'action.hover'
}}
>
<Avatar src={commentaire.auteurAvatarUrl} alt={auteur} />
<Box sx={{flex: 1, minWidth: 0}}>
<Stack direction='row' alignItems='center' flexWrap='wrap' rowGap={0.5}>
<Stack direction='row' alignItems='center' spacing={1}>
<Typography variant='subtitle2' component='div'>
{estFedere && commentaire.auteurProfilUrl ? (
<Link href={commentaire.auteurProfilUrl} target='_blank' rel='noopener noreferrer'>
{auteur}
</Link>
) : auteur}
</Typography>
{estFedere && (
<Chip
label='Mastodon'
size='small'
icon={<Mastodon size={14} color='#6364FF' title='' />}
/>
)}
</Stack>
{dat && (
<Typography variant='caption' color='text.secondary' sx={{ml: 'auto', pl: 2}}>
{dat}
</Typography>
)}
</Stack>
<Typography variant='body2' sx={{mt: 0.5}}>{metanValèMansyon(commentaire.contenu)}</Typography>
{estFedere && commentaire.remoteUrl && (
<Link href={commentaire.remoteUrl} target='_blank' rel='noopener noreferrer'>
<Typography variant='caption' color='text.secondary'>Voir sur Mastodon</Typography>
</Link> </Link>
) : auteur}
{estFedere && (
<Chip label='Mastodon' size='small' sx={{ml: 1}} />
)} )}
</Typography> </Box>
<Typography variant='body2'>{commentaire.contenu}</Typography>
{estFedere && commentaire.remoteUrl && (
<Link href={commentaire.remoteUrl} target='_blank' rel='noopener noreferrer'>
<Typography variant='caption' color='text.secondary'>Voir sur Mastodon</Typography>
</Link>
)}
</Box> </Box>
</Box> )
) })}
})} </Stack>
</Box> </Box>
) )
} }
+87 -24
View File
@@ -10,11 +10,24 @@ import DialogContent from '@mui/material/DialogContent'
import DialogActions from '@mui/material/DialogActions' import DialogActions from '@mui/material/DialogActions'
import TextField from '@mui/material/TextField' import TextField from '@mui/material/TextField'
import Typography from '@mui/material/Typography' import Typography from '@mui/material/Typography'
import Link from 'next/link' import FormControl from '@mui/material/FormControl'
import InputLabel from '@mui/material/InputLabel'
import Select from '@mui/material/Select'
import MenuItem from '@mui/material/MenuItem'
import Link from '@mui/material/Link'
import {Mastodon} from '@icons-pack/react-simple-icons'
const BOKANTE_URL = process.env.NEXT_PUBLIC_BOKANTE_URL || 'https://bokante.o-k-i.net' const BOKANTE_URL = process.env.NEXT_PUBLIC_BOKANTE_URL || 'https://bokante.o-k-i.net'
const BOKANTE_ACCOUNT = process.env.NEXT_PUBLIC_BOKANTE_ACCOUNT || 'pawol_nu' const BOKANTE_ACCOUNT = process.env.NEXT_PUBLIC_BOKANTE_ACCOUNT || 'pawol_nu'
const INSTANCES_SUGGEREES = [
{domain: 'bokante.o-k-i.net', label: 'BOKANTE - notre instance'},
{domain: 'mastodon.social', label: 'mastodon.social'},
{domain: 'piaille.fr', label: 'piaille.fr'},
]
const AUTRE_INSTANCE = 'autre'
function extraireInstance(saisie) { function extraireInstance(saisie) {
const valeur = saisie.trim().replace(/^@/, '').replace(/^https?:\/\//, '').replace(/\/$/, '') const valeur = saisie.trim().replace(/^@/, '').replace(/^https?:\/\//, '').replace(/\/$/, '')
return valeur.includes('@') ? valeur.split('@').pop() : valeur return valeur.includes('@') ? valeur.split('@').pop() : valeur
@@ -22,7 +35,8 @@ function extraireInstance(saisie) {
export default function ReponsMastodon({bokanteStatusId}) { export default function ReponsMastodon({bokanteStatusId}) {
const [ouvert, setOuvert] = useState(false) const [ouvert, setOuvert] = useState(false)
const [saisie, setSaisie] = useState('') const [instance, setInstance] = useState(INSTANCES_SUGGEREES[0].domain)
const [autreInstance, setAutreInstance] = useState('')
if (!bokanteStatusId) { if (!bokanteStatusId) {
return null return null
@@ -31,43 +45,92 @@ export default function ReponsMastodon({bokanteStatusId}) {
const tootUrl = `${BOKANTE_URL}/@${BOKANTE_ACCOUNT}/${bokanteStatusId}` const tootUrl = `${BOKANTE_URL}/@${BOKANTE_ACCOUNT}/${bokanteStatusId}`
const repondre = () => { const repondre = () => {
const instance = extraireInstance(saisie) const domaine = instance === AUTRE_INSTANCE ? extraireInstance(autreInstance) : instance
if (!instance) { if (!domaine) {
return return
} }
window.open(`https://${instance}/authorize_interaction?uri=${encodeURIComponent(tootUrl)}`, '_blank', 'noopener,noreferrer') window.open(`https://${domaine}/authorize_interaction?uri=${encodeURIComponent(tootUrl)}`, '_blank', 'noopener,noreferrer')
setOuvert(false) setOuvert(false)
setAutreInstance('')
} }
return ( return (
<Box sx={{maxWidth: 700, margin: '1em auto 0', textAlign: 'center'}}> <Box sx={{maxWidth: 700, margin: '1em auto 0', textAlign: 'center'}}>
<Typography variant='body2' sx={{mb: 1}}> <Typography variant='body2' sx={{mb: 1}}>
Rejoignez la discussion sur{' '} Rejoignez la discussion sur le Fediverse via{' '}
<Link href={tootUrl} target='_blank' rel='noopener noreferrer'>Mastodon</Link> <Link
href={tootUrl}
target='_blank'
rel='noopener noreferrer'
underline='hover'
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
px: 1,
py: 0.25,
borderRadius: 1,
bgcolor: 'action.hover',
color: 'primary.main',
fontWeight: 500,
textDecoration: 'none',
'&:hover': {
bgcolor: 'action.selected'
}
}}
>
BOKANTE (Mastodon)
</Link>
</Typography> </Typography>
<Button variant='outlined' size='small' onClick={() => setOuvert(true)}> <Button
Répondre via Mastodon variant='outlined'
size='small'
startIcon={<Mastodon size={16} color='#6364FF' title='' />}
onClick={() => setOuvert(true)}
>
Répondre sur Mastodon
</Button> </Button>
<Dialog open={ouvert} onClose={() => setOuvert(false)}> <Dialog open={ouvert} onClose={() => setOuvert(false)}>
<DialogTitle>Répondre depuis votre compte Mastodon</DialogTitle> <DialogTitle>Répondre depuis Mastodon</DialogTitle>
<DialogContent> <DialogContent>
<Typography variant='body2' sx={{mb: 2}}> <Typography variant='body2' sx={{mb: 2}}>
Indiquez votre identifiant (@vous@votre-instance) ou juste le nom de votre Choisissez linstance Mastodon sur laquelle vous avez un compte.
instance, vous serez redirigé·e vers votre compte pour répondre. BOKANTE est linstance de OKI : vous pouvez aussi utiliser
nimporte quelle autre instance du Fediverse.
</Typography> </Typography>
<TextField <FormControl fullWidth sx={{mb: 2}}>
autoFocus <InputLabel id='instance-mastodon-label'>Instance Mastodon</InputLabel>
fullWidth <Select
label='@vous@votre-instance' labelId='instance-mastodon-label'
value={saisie} id='instance-mastodon'
onChange={event => setSaisie(event.target.value)} value={instance}
onKeyDown={event => { label='Instance Mastodon'
if (event.key === 'Enter') { onChange={event => setInstance(event.target.value)}
repondre() >
} {INSTANCES_SUGGEREES.map(({domain, label}) => (
}} <MenuItem key={domain} value={domain}>
/> {label}
</MenuItem>
))}
<MenuItem value={AUTRE_INSTANCE}>Autre instance</MenuItem>
</Select>
</FormControl>
{instance === AUTRE_INSTANCE && (
<TextField
autoFocus
fullWidth
label='@vous@votre-instance'
helperText='Indiquez votre identifiant (@vous@votre-instance) ou juste le nom de votre instance, vous serez redirigé·e vers votre compte pour répondre.'
value={autreInstance}
onChange={event => setAutreInstance(event.target.value)}
onKeyDown={event => {
if (event.key === 'Enter') {
repondre()
}
}}
/>
)}
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={() => setOuvert(false)}>Annuler</Button> <Button onClick={() => setOuvert(false)}>Annuler</Button>
+11 -5
View File
@@ -3,8 +3,6 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import {useRouter} from 'next/navigation' import {useRouter} from 'next/navigation'
import Image from 'next/image' import Image from 'next/image'
import {format} from 'date-fns'
import {fr} from 'date-fns/locale'
import Card from '@mui/material/Card' import Card from '@mui/material/Card'
import CardActionArea from '@mui/material/CardActionArea' import CardActionArea from '@mui/material/CardActionArea'
@@ -13,6 +11,7 @@ import Typography from '@mui/material/Typography'
import Box from '@mui/material/Box' import Box from '@mui/material/Box'
import Grid from '@mui/material/Grid' import Grid from '@mui/material/Grid'
import ExplicitIcon from '@mui/icons-material/Explicit' import ExplicitIcon from '@mui/icons-material/Explicit'
import EventIcon from '@mui/icons-material/Event'
import {styled} from '@mui/material/styles' import {styled} from '@mui/material/styles'
import {getAlias} from '../../lib/utils/get-alias' import {getAlias} from '../../lib/utils/get-alias'
@@ -37,7 +36,7 @@ export default function TeksKat({parole}) {
const router = useRouter() const router = useRouter()
const {titre, artistes, annee, couverture, createdAt, slug} = parole const {titre, artistes, annee, couverture, createdAt, slug} = parole
const datPiblikasyon = format(new Date(createdAt), 'P', {locale: fr}) const datPiblikasyon = new Intl.DateTimeFormat(undefined, {dateStyle: 'short'}).format(new Date(createdAt))
const aliases = getAlias(artistes, parole.prioriteArtistes) const aliases = getAlias(artistes, parole.prioriteArtistes)
const handleClick = slug => { const handleClick = slug => {
@@ -103,8 +102,15 @@ export default function TeksKat({parole}) {
</> </>
)} )}
</Typography> </Typography>
<Typography align='center' style={{marginTop: '0.5em'}} variant='body1' color='textSecondary' component='p'> <Typography
Publiée le : {datPiblikasyon} align='center'
style={{marginTop: '0.5em'}}
variant='body1'
color='textSecondary'
component='p'
sx={{display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 0.5}}
>
<EventIcon fontSize='inherit' /> {datPiblikasyon}
</Typography> </Typography>
</Box> </Box>
</CardContent> </CardContent>
+91 -46
View File
@@ -3,8 +3,11 @@
import {useEffect, useState} from 'react' import {useEffect, useState} from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import Box from '@mui/material/Box' import Box from '@mui/material/Box'
import Tabs from '@mui/material/Tabs' import Select from '@mui/material/Select'
import Tab from '@mui/material/Tab' import MenuItem from '@mui/material/MenuItem'
import ListSubheader from '@mui/material/ListSubheader'
import Divider from '@mui/material/Divider'
import FormControl from '@mui/material/FormControl'
import Typography from '@mui/material/Typography' import Typography from '@mui/material/Typography'
import Tooltip from '@mui/material/Tooltip' import Tooltip from '@mui/material/Tooltip'
import {useMediaQuery} from '@mui/material' import {useMediaQuery} from '@mui/material'
@@ -13,8 +16,9 @@ import slugify from 'slugify'
import {styled} from '@mui/material/styles' import {styled} from '@mui/material/styles'
import ExplicitIcon from '@mui/icons-material/Explicit' import ExplicitIcon from '@mui/icons-material/Explicit'
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew' import TranslateIcon from '@mui/icons-material/Translate'
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos' import StarIcon from '@mui/icons-material/Star'
import ArticleIcon from '@mui/icons-material/Article'
import Image from 'next/image' import Image from 'next/image'
@@ -91,15 +95,34 @@ const LANG_NAMES = {
pt: 'Português', ja: '日本語', ko: '한국어', pt: 'Português', ja: '日本語', ko: '한국어',
} }
const CONTINENTS = ['Afrique', 'Asie', 'Europe']
const TRAD_FIELDS = [ const TRAD_FIELDS = [
{field: 'anglais', title: 'English'}, // Afrique — langues vedettes en premier
{field: 'francais', title: 'Français'}, {field: 'yoruba', title: 'Yorùbá', continent: 'Afrique', vedette: true},
{field: 'espagnol', title: 'Español'}, {field: 'lingala', title: 'Lingála', continent: 'Afrique', vedette: true},
{field: 'allemand', title: 'Deutsch'}, {field: 'wolof', title: 'Wolof', continent: 'Afrique', vedette: true},
{field: 'italien', title: 'Italiano'}, {field: 'swahili', title: 'Kiswahili', continent: 'Afrique', vedette: true},
{field: 'portugais', title: 'Português'}, {field: 'hausa', title: 'Hausa', continent: 'Afrique', vedette: true},
{field: 'japonais', title: '日本語'}, {field: 'arabe', title: 'العربية', continent: 'Afrique'},
{field: 'coreen', title: '한국어'}, {field: 'oromo', title: 'Oromoo', continent: 'Afrique'},
{field: 'igbo', title: 'Igbo', continent: 'Afrique'},
{field: 'zoulou', title: 'isiZulu', continent: 'Afrique'},
{field: 'malgache', title: 'Malagasy', continent: 'Afrique'},
{field: 'xhosa', title: 'isiXhosa', continent: 'Afrique'},
{field: 'tswana', title: 'Setswana', continent: 'Afrique'},
{field: 'tsonga', title: 'Xitsonga', continent: 'Afrique'},
{field: 'sesotho', title: 'Sesotho', continent: 'Afrique'},
// Asie
{field: 'japonais', title: '日本語', continent: 'Asie'},
{field: 'coreen', title: '한국어', continent: 'Asie'},
// Europe
{field: 'anglais', title: 'English', continent: 'Europe'},
{field: 'francais', title: 'Français', continent: 'Europe'},
{field: 'espagnol', title: 'Español', continent: 'Europe'},
{field: 'allemand', title: 'Deutsch', continent: 'Europe'},
{field: 'italien', title: 'Italiano', continent: 'Europe'},
{field: 'portugais', title: 'Português', continent: 'Europe'},
] ]
const langToArray = parole => { const langToArray = parole => {
@@ -109,7 +132,7 @@ const langToArray = parole => {
return TRAD_FIELDS return TRAD_FIELDS
.filter(({field}) => parole.traductions[field]) .filter(({field}) => parole.traductions[field])
.map(({field, title}) => ({title, lang: parole.traductions[field]})) .map(({field, title, continent, vedette}) => ({field, title, continent, vedette, lang: parole.traductions[field]}))
} }
const ExplicitTooltip = Tooltip const ExplicitTooltip = Tooltip
@@ -230,39 +253,61 @@ export default function Teks({parole}) {
)} )}
</Box> </Box>
<Box sx={{maxWidth: 700, margin: '0 auto'}} className={classes.gridText}> <Box sx={{maxWidth: 700, margin: '0 auto'}} className={classes.gridText}>
<Tabs <Box sx={{display: 'flex', justifyContent: 'center', borderBottom: 1, borderColor: 'divider', mb: 2, pb: 2}}>
scrollButtons <FormControl size='small' sx={{minWidth: 240, width: isMobile ? '100%' : 'auto'}}>
allowScrollButtonsMobile <Select
value={tab} value={tab}
variant='scrollable' MenuProps={{PaperProps: {sx: {maxHeight: 420}}}}
centered={langArray.length === 0} renderValue={value => {
slots={{ const label = value === 0 ? 'Transcription' : langArray[value - 1]?.title
startScrollButtonIcon: ArrowBackIosNewIcon, const Icon = value === 0 ? ArticleIcon : TranslateIcon
endScrollButtonIcon: ArrowForwardIosIcon return (
}} <Box sx={{display: 'flex', alignItems: 'center', gap: 1, fontWeight: value === 0 ? 700 : 400}}>
sx={{ <Icon fontSize='small' color='primary' />
borderBottom: 1, {label}
borderColor: 'divider', </Box>
mb: 2, )
'& .MuiTabs-scrollButtons': { }}
width: 36, onChange={event => setTab(event.target.value)}
height: 36, >
borderRadius: '50%', <MenuItem value={0} sx={{display: 'flex', gap: 0.75, fontWeight: 700}}>
color: 'primary.main', <ArticleIcon fontSize='inherit' color='primary' />
backgroundColor: 'action.hover', Transcription
mx: 0.5, </MenuItem>
'&.Mui-disabled': { <Divider />
opacity: 0 {CONTINENTS.flatMap(continent => {
} const items = langArray
} .map((item, index) => ({...item, index}))
}} .filter(item => item.continent === continent)
onChange={(event, value) => setTab(value)}
> if (items.length === 0) {
<Tab label='Transcription' /> return []
{langArray.map(({title}) => ( }
<Tab key={title} label={title} />
))} return [
</Tabs> <ListSubheader
key={`entet-${continent}`}
sx={{
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: 1,
color: 'primary.main',
lineHeight: '2.5em'
}}
>
{continent}
</ListSubheader>,
...items.map(({title, index, vedette}) => (
<MenuItem key={title} value={index + 1} sx={{display: 'flex', gap: 0.75}}>
{vedette && <StarIcon fontSize='inherit' color='primary' />}
{title}
</MenuItem>
))
]
})}
</Select>
</FormControl>
</Box>
{tab === 0 && ( {tab === 0 && (
<> <>
{parole.langueSource && parole.langueSource !== 'ka' && ( {parole.langueSource && parole.langueSource !== 'ka' && (
+1 -1
View File
@@ -2,7 +2,7 @@ import qs from 'qs'
const OKI_API = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:1337' const OKI_API = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:1337'
const AWTIS_POU_CHAK_PAJ = process.env.NEXT_PUBLIC_AWTIS_POU_CHAK_PAJ || 6 const AWTIS_POU_CHAK_PAJ = process.env.NEXT_PUBLIC_AWTIS_POU_CHAK_PAJ || 6
const readToken = process.env.NEXT_PUBLIC_READ_TOKEN || 'read-token' const readToken = process.env.API_READ_TOKEN || 'read-token'
const headers = { const headers = {
next: {revalidate: 60}, next: {revalidate: 60},