Compare commits
27
Commits
64cdd784c3
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4cc5c405bb
|
||
|
|
c8841a253f
|
||
|
|
4e6341c5d5
|
||
|
|
b5a3b278ba
|
||
|
|
99daeb014c
|
||
|
|
f5a4014e41
|
||
|
|
d66c679fd1
|
||
|
|
cab293d62e | ||
|
|
5936de59ff
|
||
|
|
6804e3626f
|
||
|
|
461b063cff
|
||
|
|
6601b00545
|
||
|
|
f88b0184ea
|
||
|
|
43964ec91d
|
||
|
|
ce19fa3b5d
|
||
|
|
33514bf633
|
||
|
|
35c39a97f3
|
||
|
|
72b86fdc5c
|
||
|
|
53287f9c80
|
||
|
|
8979970a90
|
||
|
|
75c5979478
|
||
|
|
5778d865e2
|
||
|
|
0ac18faf5c | ||
|
|
21248b7138
|
||
|
|
eda30345c6
|
||
|
|
b40e2d71b0
|
||
|
|
4bc545ac72
|
+6
-1
@@ -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.
|
||||||
@@ -81,6 +81,11 @@ NEXT_PUBLIC_GIT=
|
|||||||
NEXT_PUBLIC_CODEBERG=
|
NEXT_PUBLIC_CODEBERG=
|
||||||
NEXT_PUBLIC_BLUESKY_URL=
|
NEXT_PUBLIC_BLUESKY_URL=
|
||||||
|
|
||||||
|
# Commentaires fédérés (bokante.o-k-i.net)
|
||||||
|
|
||||||
|
NEXT_PUBLIC_BOKANTE_URL=https://bokante.o-k-i.net
|
||||||
|
NEXT_PUBLIC_BOKANTE_ACCOUNT=pawol_nu
|
||||||
|
|
||||||
# DOMAIN IMAGE
|
# DOMAIN IMAGE
|
||||||
NEXT_PUBLIC_DOMAINS_IMAGE="localhost:1337 strapi.mondomaine.com"
|
NEXT_PUBLIC_DOMAINS_IMAGE="localhost:1337 strapi.mondomaine.com"
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ jobs:
|
|||||||
username: ${{ secrets.SSH_USER }}
|
username: ${{ secrets.SSH_USER }}
|
||||||
key: ${{ secrets.SSH_KEY }}
|
key: ${{ secrets.SSH_KEY }}
|
||||||
script: |
|
script: |
|
||||||
|
set -e
|
||||||
export NVM_DIR="$HOME/.nvm"
|
export NVM_DIR="$HOME/.nvm"
|
||||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||||
cd ${{ secrets.FRONT_DEPLOY_PATH }}
|
cd ${{ secrets.FRONT_DEPLOY_PATH }}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ jobs:
|
|||||||
username: ${{ secrets.SSH_USER }}
|
username: ${{ secrets.SSH_USER }}
|
||||||
key: ${{ secrets.SSH_KEY }}
|
key: ${{ secrets.SSH_KEY }}
|
||||||
script: |
|
script: |
|
||||||
|
set -e
|
||||||
export NVM_DIR="$HOME/.nvm"
|
export NVM_DIR="$HOME/.nvm"
|
||||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||||
cd ${{ secrets.FRONT_DEPLOY_PATH }}
|
cd ${{ secrets.FRONT_DEPLOY_PATH }}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ jobs:
|
|||||||
username: ${{ secrets.SSH_USER }}
|
username: ${{ secrets.SSH_USER }}
|
||||||
key: ${{ secrets.SSH_KEY }}
|
key: ${{ secrets.SSH_KEY }}
|
||||||
script: |
|
script: |
|
||||||
|
set -e
|
||||||
export NVM_DIR="$HOME/.nvm"
|
export NVM_DIR="$HOME/.nvm"
|
||||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||||
cd ${{ secrets.PROD_DEPLOY_PATH }}
|
cd ${{ secrets.PROD_DEPLOY_PATH }}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import {jwennToutAwtis} from '../../../lib/oki-api'
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const data = await jwennToutAwtis()
|
||||||
|
return Response.json(data)
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import {GET} from '../route'
|
|||||||
function fakeUpstreamResponse({status = 200, headers = {}, body = 'audio-bytes'} = {}) {
|
function fakeUpstreamResponse({status = 200, headers = {}, body = 'audio-bytes'} = {}) {
|
||||||
return {
|
return {
|
||||||
status,
|
status,
|
||||||
|
ok: status >= 200 && status < 300,
|
||||||
body,
|
body,
|
||||||
headers: {
|
headers: {
|
||||||
get: key => headers[key.toLowerCase()] ?? null
|
get: key => headers[key.toLowerCase()] ?? null
|
||||||
@@ -52,4 +53,16 @@ describe('GET /api/mizik-stream/[id]', () => {
|
|||||||
expect(response.status).toBe(200)
|
expect(response.status).toBe(200)
|
||||||
expect(response.headers.get('content-type')).toBe('audio/mpeg')
|
expect(response.headers.get('content-type')).toBe('audio/mpeg')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('renvoie une erreur JSON si le serveur audio répond en erreur', async () => {
|
||||||
|
global.fetch = vi.fn(async () => fakeUpstreamResponse({status: 401, headers: {'content-type': 'application/json'}, body: '{"error": "unauthorized"}'}))
|
||||||
|
|
||||||
|
const request = new Request('http://localhost/api/mizik-stream/7')
|
||||||
|
const response = await GET(request, {params: Promise.resolve({id: '7'})})
|
||||||
|
|
||||||
|
expect(response.status).toBe(401)
|
||||||
|
expect(response.headers.get('content-type')).toContain('application/json')
|
||||||
|
const json = await response.json()
|
||||||
|
expect(json.error).toContain('401')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
const MIZIK_URL = process.env.NEXT_PUBLIC_OKI_MIZIK_URL || 'https://funkwhale-server.com'
|
const MIZIK_URL = process.env.NEXT_PUBLIC_OKI_MIZIK_URL || 'https://funkwhale-server.com'
|
||||||
const MIZIK_API_USER = process.env.MIZIK_API_USER || 'user'
|
// Les variables publiques sont conservées en fallback pendant la migration
|
||||||
const MIZIK_API_PASSWORD = process.env.MIZIK_API_PASSWORD || 'password'
|
// des déploiements qui utiliseraient encore l’ancien nommage.
|
||||||
|
const MIZIK_API_USER = process.env.MIZIK_API_USER || process.env.NEXT_PUBLIC_MIZIK_API_USER || 'user'
|
||||||
|
const MIZIK_API_PASSWORD = process.env.MIZIK_API_PASSWORD || process.env.NEXT_PUBLIC_MIZIK_API_PASSWORD || 'password'
|
||||||
|
|
||||||
const FORWARDED_HEADERS = ['content-type', 'content-length', 'content-range', 'accept-ranges']
|
const FORWARDED_HEADERS = ['content-type', 'content-length', 'content-range', 'accept-ranges']
|
||||||
|
|
||||||
@@ -14,6 +16,12 @@ export async function GET(request, props) {
|
|||||||
headers: range ? {range} : {}
|
headers: range ? {range} : {}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (!upstreamResponse.ok) {
|
||||||
|
const message = `Le serveur audio a répondu ${upstreamResponse.status}`
|
||||||
|
console.error(message, upstreamUrl.replace(/p=[^&]+/, 'p=***'))
|
||||||
|
return Response.json({error: message}, {status: upstreamResponse.status})
|
||||||
|
}
|
||||||
|
|
||||||
const headers = new Headers()
|
const headers = new Headers()
|
||||||
for (const key of FORWARDED_HEADERS) {
|
for (const key of FORWARDED_HEADERS) {
|
||||||
const value = upstreamResponse.headers.get(key)
|
const value = upstreamResponse.headers.get(key)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import {headers} from 'next/headers'
|
||||||
import {notFound} from 'next/navigation'
|
import {notFound} from 'next/navigation'
|
||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
|
|
||||||
@@ -67,6 +68,8 @@ export default async function AwtisPajSlug(props) {
|
|||||||
const params = await props.params
|
const params = await props.params
|
||||||
const {slug} = params
|
const {slug} = params
|
||||||
const anAwtis = await jwennAwtis(slug)
|
const anAwtis = await jwennAwtis(slug)
|
||||||
|
const requestHeaders = await headers()
|
||||||
|
const nonce = requestHeaders.get('x-nonce') || undefined
|
||||||
|
|
||||||
const {photo} = anAwtis
|
const {photo} = anAwtis
|
||||||
const kuvetiFormat = formatKuveti(photo)
|
const kuvetiFormat = formatKuveti(photo)
|
||||||
@@ -91,6 +94,7 @@ export default async function AwtisPajSlug(props) {
|
|||||||
<section>
|
<section>
|
||||||
<script
|
<script
|
||||||
type='application/ld+json'
|
type='application/ld+json'
|
||||||
|
nonce={nonce}
|
||||||
dangerouslySetInnerHTML={{__html: JSON.stringify(jsonLd)}}
|
dangerouslySetInnerHTML={{__html: JSON.stringify(jsonLd)}}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import {useServerInsertedHTML} from 'next/navigation'
|
|||||||
import {CacheProvider as DefaultCacheProvider} from '@emotion/react'
|
import {CacheProvider as DefaultCacheProvider} from '@emotion/react'
|
||||||
|
|
||||||
export default function NextAppDirEmotionCacheProvider(props) {
|
export default function NextAppDirEmotionCacheProvider(props) {
|
||||||
const {options, CacheProvider = DefaultCacheProvider, children} = props
|
const {options, CacheProvider = DefaultCacheProvider, children, nonce} = props
|
||||||
|
|
||||||
const [registry] = React.useState(() => {
|
const [registry] = React.useState(() => {
|
||||||
const cache = createCache(options)
|
const cache = createCache({...options, nonce})
|
||||||
cache.compat = true
|
cache.compat = true
|
||||||
const prevInsert = cache.insert
|
const prevInsert = cache.insert
|
||||||
let inserted = []
|
let inserted = []
|
||||||
@@ -65,12 +65,14 @@ export default function NextAppDirEmotionCacheProvider(props) {
|
|||||||
<style
|
<style
|
||||||
key={name}
|
key={name}
|
||||||
dangerouslySetInnerHTML={{__html: style}}
|
dangerouslySetInnerHTML={{__html: style}}
|
||||||
|
nonce={nonce}
|
||||||
data-emotion={`${registry.cache.key}-global ${name}`}
|
data-emotion={`${registry.cache.key}-global ${name}`}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{styles !== '' && (
|
{styles !== '' && (
|
||||||
<style
|
<style
|
||||||
dangerouslySetInnerHTML={{__html: styles}}
|
dangerouslySetInnerHTML={{__html: styles}}
|
||||||
|
nonce={nonce}
|
||||||
data-emotion={dataEmotionAttribute}
|
data-emotion={dataEmotionAttribute}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -84,5 +86,6 @@ export default function NextAppDirEmotionCacheProvider(props) {
|
|||||||
NextAppDirEmotionCacheProvider.propTypes = {
|
NextAppDirEmotionCacheProvider.propTypes = {
|
||||||
options: PropTypes.object.isRequired,
|
options: PropTypes.object.isRequired,
|
||||||
CacheProvider: PropTypes.func,
|
CacheProvider: PropTypes.func,
|
||||||
children: PropTypes.node.isRequired
|
children: PropTypes.node.isRequired,
|
||||||
|
nonce: PropTypes.string
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-3
@@ -1,3 +1,4 @@
|
|||||||
|
import {headers} from 'next/headers'
|
||||||
import PlausibleProvider from 'next-plausible'
|
import PlausibleProvider from 'next-plausible'
|
||||||
import TopLoader from '../components/top-loader'
|
import TopLoader from '../components/top-loader'
|
||||||
import Navigasyon from '../components/navigasyon'
|
import Navigasyon from '../components/navigasyon'
|
||||||
@@ -61,16 +62,20 @@ const jsonLd = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default async function RootLayout({children}) {
|
export default async function RootLayout({children}) {
|
||||||
|
const requestHeaders = await headers()
|
||||||
|
const nonce = requestHeaders.get('x-nonce') || undefined
|
||||||
|
|
||||||
const inner = (
|
const inner = (
|
||||||
<>
|
<>
|
||||||
<TopLoader color='#ffeb3b' />
|
<TopLoader color='#ffeb3b' nonce={nonce} />
|
||||||
<ThemeRegistry>
|
<ThemeRegistry nonce={nonce}>
|
||||||
<Navigasyon />
|
<Navigasyon />
|
||||||
{children}
|
{children}
|
||||||
</ThemeRegistry>
|
</ThemeRegistry>
|
||||||
<section>
|
<section>
|
||||||
<script
|
<script
|
||||||
type='application/ld+json'
|
type='application/ld+json'
|
||||||
|
nonce={nonce}
|
||||||
dangerouslySetInnerHTML={{__html: JSON.stringify(jsonLd)}}
|
dangerouslySetInnerHTML={{__html: JSON.stringify(jsonLd)}}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
@@ -81,7 +86,7 @@ export default async function RootLayout({children}) {
|
|||||||
<html suppressHydrationWarning lang='fr'>
|
<html suppressHydrationWarning lang='fr'>
|
||||||
<body>
|
<body>
|
||||||
{plausibleUrl
|
{plausibleUrl
|
||||||
? <PlausibleProvider src={plausibleUrl}>{inner}</PlausibleProvider>
|
? <PlausibleProvider src={plausibleUrl} scriptProps={{nonce}}>{inner}</PlausibleProvider>
|
||||||
: inner}
|
: inner}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+2
-2
@@ -1,5 +1,3 @@
|
|||||||
export const dynamic = 'force-dynamic'
|
|
||||||
|
|
||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Container from '@mui/material/Container'
|
import Container from '@mui/material/Container'
|
||||||
import {notFound} from 'next/navigation'
|
import {notFound} from 'next/navigation'
|
||||||
@@ -12,6 +10,8 @@ import AnVedette from '../components/akey/an-vedette'
|
|||||||
import Footer from '../components/footer'
|
import Footer from '../components/footer'
|
||||||
import Aso from '../components/akey/aso'
|
import Aso from '../components/akey/aso'
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
async function jwennDone() {
|
async function jwennDone() {
|
||||||
const [statistik, denyeTeks, anVedette] = await Promise.all([jwennStats(), jwennDenyeTeks(), jwennAnVedette()])
|
const [statistik, denyeTeks, anVedette] = await Promise.all([jwennStats(), jwennDenyeTeks(), jwennAnVedette()])
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import {headers} from 'next/headers'
|
||||||
import {notFound} from 'next/navigation'
|
import {notFound} from 'next/navigation'
|
||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
|
|
||||||
@@ -71,6 +72,8 @@ export default async function AnPawolPaj(props) {
|
|||||||
const {slug} = params
|
const {slug} = params
|
||||||
|
|
||||||
const anTeks = await jwennAnTeks(slug)
|
const anTeks = await jwennAnTeks(slug)
|
||||||
|
const requestHeaders = await headers()
|
||||||
|
const nonce = requestHeaders.get('x-nonce') || undefined
|
||||||
const {couverture} = anTeks
|
const {couverture} = anTeks
|
||||||
const teksKuvetiFormat = formatKuveti(couverture)
|
const teksKuvetiFormat = formatKuveti(couverture)
|
||||||
|
|
||||||
@@ -126,6 +129,7 @@ export default async function AnPawolPaj(props) {
|
|||||||
<section>
|
<section>
|
||||||
<script
|
<script
|
||||||
type='application/ld+json'
|
type='application/ld+json'
|
||||||
|
nonce={nonce}
|
||||||
dangerouslySetInnerHTML={{__html: JSON.stringify(jsonLd)}}
|
dangerouslySetInnerHTML={{__html: JSON.stringify(jsonLd)}}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -64,12 +64,12 @@ const theme = createTheme({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export default function ThemeRegistry(props) {
|
export default function ThemeRegistry(props) {
|
||||||
const {children} = props
|
const {children, nonce} = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<InitColorSchemeScript attribute='class' />
|
<InitColorSchemeScript attribute='class' nonce={nonce} />
|
||||||
<NextAppDirEmotionCacheProvider options={{key: 'mui'}}>
|
<NextAppDirEmotionCacheProvider options={{key: 'mui'}} nonce={nonce}>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<CssBaseline enableColorScheme />
|
<CssBaseline enableColorScheme />
|
||||||
<ChanjeTem />
|
<ChanjeTem />
|
||||||
@@ -81,5 +81,6 @@ export default function ThemeRegistry(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ThemeRegistry.propTypes = {
|
ThemeRegistry.propTypes = {
|
||||||
children: PropTypes.node.isRequired
|
children: PropTypes.node.isRequired,
|
||||||
|
nonce: PropTypes.string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import {describe, it, expect} from 'vitest'
|
||||||
|
import {render, screen} from '@testing-library/react'
|
||||||
|
import Komante from '../komante'
|
||||||
|
|
||||||
|
describe('Komante', () => {
|
||||||
|
it('ne rend rien quand il n\'y a aucun commentaire', () => {
|
||||||
|
const {container} = render(<Komante commentaires={[]} />)
|
||||||
|
expect(container).toBeEmptyDOMElement()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('affiche un commentaire local avec le pseudo pawol.nu', () => {
|
||||||
|
render(<Komante commentaires={[
|
||||||
|
{id: 1, contenu: 'Bèl tèks', origine: 'local', user: {username: 'foo'}}
|
||||||
|
]} />)
|
||||||
|
|
||||||
|
expect(screen.getByText('foo')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Bèl tèks')).toBeInTheDocument()
|
||||||
|
expect(screen.queryByText('Mastodon')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('affiche un commentaire fédéré avec le badge et le lien Mastodon', () => {
|
||||||
|
render(<Komante commentaires={[{
|
||||||
|
id: 2,
|
||||||
|
contenu: 'Trè bèl',
|
||||||
|
origine: 'activitypub',
|
||||||
|
auteurNom: 'Quelqu\'un',
|
||||||
|
auteurHandle: '@quelqun@mastodon.social',
|
||||||
|
auteurProfilUrl: 'https://mastodon.social/@quelqun',
|
||||||
|
remoteUrl: 'https://bokante.o-k-i.net/@quelqun/1'
|
||||||
|
}]} />)
|
||||||
|
|
||||||
|
expect(screen.getByText('Quelqu\'un')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Mastodon')).toBeInTheDocument()
|
||||||
|
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')
|
||||||
|
})
|
||||||
|
|
||||||
|
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()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import {describe, it, expect, vi, afterEach} from 'vitest'
|
||||||
|
import {render, screen} from '@testing-library/react'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import ReponsMastodon from '../repons-mastodon'
|
||||||
|
|
||||||
|
describe('ReponsMastodon', () => {
|
||||||
|
const originalOpen = window.open
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
window.open = originalOpen
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ne rend rien sans bokanteStatusId', () => {
|
||||||
|
const {container} = render(<ReponsMastodon bokanteStatusId={null} />)
|
||||||
|
expect(container).toBeEmptyDOMElement()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('affiche un lien direct vers le fil BOKANTE', () => {
|
||||||
|
render(<ReponsMastodon bokanteStatusId='112233' />)
|
||||||
|
const lien = screen.getByRole('link', {name: 'BOKANTE (Mastodon)'})
|
||||||
|
expect(lien).toHaveAttribute('href', 'https://bokante.o-k-i.net/@pawol_nu/112233')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('redirige vers BOKANTE par défaut quand on continue', 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.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}))
|
||||||
|
|
||||||
|
expect(window.open).toHaveBeenCalledWith(
|
||||||
|
'https://mastodon.social/authorize_interaction?uri=' + encodeURIComponent('https://bokante.o-k-i.net/@pawol_nu/112233'),
|
||||||
|
'_blank',
|
||||||
|
'noopener,noreferrer'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('accepte un identifiant complet pour une autre instance', 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), '@quelqun@mastodon.social')
|
||||||
|
await user.click(screen.getByRole('button', {name: /continuer/i}))
|
||||||
|
|
||||||
|
expect(window.open).toHaveBeenCalledWith(
|
||||||
|
'https://mastodon.social/authorize_interaction?uri=' + encodeURIComponent('https://bokante.o-k-i.net/@pawol_nu/112233'),
|
||||||
|
'_blank',
|
||||||
|
'noopener,noreferrer'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -8,7 +8,6 @@ export default function AnTeks({parole, paroleId}) {
|
|||||||
<Teks
|
<Teks
|
||||||
parole={parole}
|
parole={parole}
|
||||||
paroleId={paroleId}
|
paroleId={paroleId}
|
||||||
commentaires={parole?.commentaires}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import Box from '@mui/material/Box'
|
||||||
|
import Stack from '@mui/material/Stack'
|
||||||
|
import Avatar from '@mui/material/Avatar'
|
||||||
|
import Typography from '@mui/material/Typography'
|
||||||
|
import Chip from '@mui/material/Chip'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import {Mastodon} from '@icons-pack/react-simple-icons'
|
||||||
|
|
||||||
|
function formatAuteur(commentaire) {
|
||||||
|
if (commentaire.origine === 'activitypub') {
|
||||||
|
return commentaire.auteurNom || commentaire.auteurHandle || 'Yon moun'
|
||||||
|
}
|
||||||
|
|
||||||
|
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}) {
|
||||||
|
if (!commentaires || commentaires.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{maxWidth: 700, margin: '2em auto 0'}}>
|
||||||
|
<Typography gutterBottom variant='h6'>
|
||||||
|
Komantè ({commentaires.length})
|
||||||
|
</Typography>
|
||||||
|
<Stack spacing={1.5}>
|
||||||
|
{commentaires.map(commentaire => {
|
||||||
|
const auteur = formatAuteur(commentaire)
|
||||||
|
const estFedere = commentaire.origine === 'activitypub'
|
||||||
|
const dat = formatDat(commentaire)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
key={commentaire.id}
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: 1.5,
|
||||||
|
p: 2,
|
||||||
|
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>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Komante.propTypes = {
|
||||||
|
commentaires: PropTypes.array
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import {useState} from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import Box from '@mui/material/Box'
|
||||||
|
import Button from '@mui/material/Button'
|
||||||
|
import Dialog from '@mui/material/Dialog'
|
||||||
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
|
import DialogContent from '@mui/material/DialogContent'
|
||||||
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
|
import TextField from '@mui/material/TextField'
|
||||||
|
import Typography from '@mui/material/Typography'
|
||||||
|
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_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) {
|
||||||
|
const valeur = saisie.trim().replace(/^@/, '').replace(/^https?:\/\//, '').replace(/\/$/, '')
|
||||||
|
return valeur.includes('@') ? valeur.split('@').pop() : valeur
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ReponsMastodon({bokanteStatusId}) {
|
||||||
|
const [ouvert, setOuvert] = useState(false)
|
||||||
|
const [instance, setInstance] = useState(INSTANCES_SUGGEREES[0].domain)
|
||||||
|
const [autreInstance, setAutreInstance] = useState('')
|
||||||
|
|
||||||
|
if (!bokanteStatusId) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const tootUrl = `${BOKANTE_URL}/@${BOKANTE_ACCOUNT}/${bokanteStatusId}`
|
||||||
|
|
||||||
|
const repondre = () => {
|
||||||
|
const domaine = instance === AUTRE_INSTANCE ? extraireInstance(autreInstance) : instance
|
||||||
|
if (!domaine) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
window.open(`https://${domaine}/authorize_interaction?uri=${encodeURIComponent(tootUrl)}`, '_blank', 'noopener,noreferrer')
|
||||||
|
setOuvert(false)
|
||||||
|
setAutreInstance('')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{maxWidth: 700, margin: '1em auto 0', textAlign: 'center'}}>
|
||||||
|
<Typography variant='body2' sx={{mb: 1}}>
|
||||||
|
Rejoignez la discussion sur le Fediverse via{' '}
|
||||||
|
<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>
|
||||||
|
<Button
|
||||||
|
variant='outlined'
|
||||||
|
size='small'
|
||||||
|
startIcon={<Mastodon size={16} color='#6364FF' title='' />}
|
||||||
|
onClick={() => setOuvert(true)}
|
||||||
|
>
|
||||||
|
Répondre sur Mastodon
|
||||||
|
</Button>
|
||||||
|
<Dialog open={ouvert} onClose={() => setOuvert(false)}>
|
||||||
|
<DialogTitle>Répondre depuis Mastodon</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Typography variant='body2' sx={{mb: 2}}>
|
||||||
|
Choisissez l’instance Mastodon sur laquelle vous avez un compte.
|
||||||
|
BOKANTE est l’instance de OKI : vous pouvez aussi utiliser
|
||||||
|
n’importe quelle autre instance du Fediverse.
|
||||||
|
</Typography>
|
||||||
|
<FormControl fullWidth sx={{mb: 2}}>
|
||||||
|
<InputLabel id='instance-mastodon-label'>Instance Mastodon</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId='instance-mastodon-label'
|
||||||
|
id='instance-mastodon'
|
||||||
|
value={instance}
|
||||||
|
label='Instance Mastodon'
|
||||||
|
onChange={event => setInstance(event.target.value)}
|
||||||
|
>
|
||||||
|
{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>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => setOuvert(false)}>Annuler</Button>
|
||||||
|
<Button variant='contained' onClick={repondre}>Continuer</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ReponsMastodon.propTypes = {
|
||||||
|
bokanteStatusId: PropTypes.string
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
+93
-43
@@ -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'
|
||||||
|
|
||||||
@@ -29,6 +33,8 @@ import {StreamButton} from '../streaming-buttons'
|
|||||||
import EntegreMizik from './entegre-mizik'
|
import EntegreMizik from './entegre-mizik'
|
||||||
import OkiMizik from './oki-mizik'
|
import OkiMizik from './oki-mizik'
|
||||||
import DiferansDialog from './diferans-dialog'
|
import DiferansDialog from './diferans-dialog'
|
||||||
|
import Komante from './komante'
|
||||||
|
import ReponsMastodon from './repons-mastodon'
|
||||||
import {useCurrentTrack} from './current-track-context'
|
import {useCurrentTrack} from './current-track-context'
|
||||||
|
|
||||||
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'
|
||||||
@@ -89,15 +95,35 @@ 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: 'ayisyen', title: 'Ayisyen 🇭🇹', continent: 'Afrique', vedette: true},
|
||||||
{field: 'espagnol', title: 'Español'},
|
{field: 'yoruba', title: 'Yorùbá', continent: 'Afrique', vedette: true},
|
||||||
{field: 'allemand', title: 'Deutsch'},
|
{field: 'lingala', title: 'Lingála', continent: 'Afrique', vedette: true},
|
||||||
{field: 'italien', title: 'Italiano'},
|
{field: 'wolof', title: 'Wolof', continent: 'Afrique', vedette: true},
|
||||||
{field: 'portugais', title: 'Português'},
|
{field: 'swahili', title: 'Kiswahili', continent: 'Afrique', vedette: true},
|
||||||
{field: 'japonais', title: '日本語'},
|
{field: 'hausa', title: 'Hausa', continent: 'Afrique', vedette: true},
|
||||||
{field: 'coreen', title: '한국어'},
|
{field: 'arabe', title: 'العربية', continent: 'Afrique'},
|
||||||
|
{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 => {
|
||||||
@@ -107,7 +133,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
|
||||||
@@ -228,39 +254,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}}>
|
||||||
|
<Icon fontSize='small' color='primary' />
|
||||||
|
{label}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
sx={{
|
onChange={event => setTab(event.target.value)}
|
||||||
borderBottom: 1,
|
|
||||||
borderColor: 'divider',
|
|
||||||
mb: 2,
|
|
||||||
'& .MuiTabs-scrollButtons': {
|
|
||||||
width: 36,
|
|
||||||
height: 36,
|
|
||||||
borderRadius: '50%',
|
|
||||||
color: 'primary.main',
|
|
||||||
backgroundColor: 'action.hover',
|
|
||||||
mx: 0.5,
|
|
||||||
'&.Mui-disabled': {
|
|
||||||
opacity: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onChange={(event, value) => setTab(value)}
|
|
||||||
>
|
>
|
||||||
<Tab label='Transcription' />
|
<MenuItem value={0} sx={{display: 'flex', gap: 0.75, fontWeight: 700}}>
|
||||||
{langArray.map(({title}) => (
|
<ArticleIcon fontSize='inherit' color='primary' />
|
||||||
<Tab key={title} label={title} />
|
Transcription
|
||||||
))}
|
</MenuItem>
|
||||||
</Tabs>
|
<Divider />
|
||||||
|
{CONTINENTS.flatMap(continent => {
|
||||||
|
const items = langArray
|
||||||
|
.map((item, index) => ({...item, index}))
|
||||||
|
.filter(item => item.continent === continent)
|
||||||
|
|
||||||
|
if (items.length === 0) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
<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' && (
|
||||||
@@ -281,6 +329,8 @@ export default function Teks({parole}) {
|
|||||||
)
|
)
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
|
<ReponsMastodon bokanteStatusId={parole.bokanteStatusId} />
|
||||||
|
<Komante commentaires={parole.commentaires} />
|
||||||
</Root>
|
</Root>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,13 @@
|
|||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import NextTopLoader from 'nextjs-toploader'
|
import NextTopLoader from 'nextjs-toploader'
|
||||||
|
|
||||||
export default function TopLoader({color}) {
|
export default function TopLoader({color, nonce}) {
|
||||||
return <NextTopLoader color={color} />
|
return <NextTopLoader color={color} nonce={nonce} />
|
||||||
|
}
|
||||||
|
|
||||||
|
TopLoader.propTypes = {
|
||||||
|
color: PropTypes.string.isRequired,
|
||||||
|
nonce: PropTypes.string
|
||||||
}
|
}
|
||||||
|
|
||||||
TopLoader.propTypes = {
|
TopLoader.propTypes = {
|
||||||
|
|||||||
+11
-2
@@ -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},
|
||||||
@@ -38,7 +38,16 @@ export async function jwennTeksEpiSlug(slug) {
|
|||||||
fields: ['username', 'firstname']
|
fields: ['username', 'firstname']
|
||||||
},
|
},
|
||||||
commentaires: {
|
commentaires: {
|
||||||
fields: ['contenu', 'datePublication'],
|
fields: [
|
||||||
|
'contenu',
|
||||||
|
'datePublication',
|
||||||
|
'origine',
|
||||||
|
'auteurNom',
|
||||||
|
'auteurHandle',
|
||||||
|
'auteurAvatarUrl',
|
||||||
|
'auteurProfilUrl',
|
||||||
|
'remoteUrl'
|
||||||
|
],
|
||||||
populate: {
|
populate: {
|
||||||
user: {
|
user: {
|
||||||
fields: ['username']
|
fields: ['username']
|
||||||
|
|||||||
+41
-1
@@ -28,9 +28,49 @@ function buildRemotePatterns() {
|
|||||||
return patterns
|
return patterns
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function headers() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: '/:path*',
|
||||||
|
headers: [
|
||||||
|
{
|
||||||
|
key: 'Strict-Transport-Security',
|
||||||
|
value: 'max-age=63072000; includeSubDomains; preload'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'X-Content-Type-Options',
|
||||||
|
value: 'nosniff'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'X-Frame-Options',
|
||||||
|
value: 'SAMEORIGIN'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Referrer-Policy',
|
||||||
|
value: 'strict-origin-when-cross-origin'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Cross-Origin-Resource-Policy',
|
||||||
|
value: 'same-site'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Cross-Origin-Opener-Policy',
|
||||||
|
value: 'same-origin'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Permissions-Policy',
|
||||||
|
value: 'accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = (withPWA({
|
module.exports = (withPWA({
|
||||||
turbopack: {},
|
turbopack: {},
|
||||||
|
poweredByHeader: false,
|
||||||
images: {
|
images: {
|
||||||
remotePatterns: buildRemotePatterns()
|
remotePatterns: buildRemotePatterns()
|
||||||
}
|
},
|
||||||
|
headers
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -53,6 +53,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
|
"@testing-library/dom": "^10.4.1",
|
||||||
"@testing-library/jest-dom": "^6.9.1",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@testing-library/react": "^16.3.2",
|
"@testing-library/react": "^16.3.2",
|
||||||
"@testing-library/user-event": "^14.6.1",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
/* eslint-disable @next/next/no-server-import-in-page */
|
||||||
|
import {NextResponse} from 'next/server'
|
||||||
|
|
||||||
|
const nonceCharset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
||||||
|
|
||||||
|
function generateNonce() {
|
||||||
|
const array = new Uint8Array(16)
|
||||||
|
crypto.getRandomValues(array)
|
||||||
|
let nonce = ''
|
||||||
|
for (const byte of array) {
|
||||||
|
nonce += nonceCharset[byte % 64]
|
||||||
|
}
|
||||||
|
|
||||||
|
return nonce
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractOrigin(url) {
|
||||||
|
if (!url) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return new URL(url).origin
|
||||||
|
} catch {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractHostSource(raw) {
|
||||||
|
if (!raw) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const [hostname, port] = raw.split(':')
|
||||||
|
if (!hostname) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hostname === 'localhost' || hostname === '127.0.0.1') {
|
||||||
|
return `http://${hostname}${port ? `:${port}` : ''}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `https://${hostname}${port ? `:${port}` : ''}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCsp(nonce) {
|
||||||
|
const apiUrl = process.env.NEXT_PUBLIC_API_URL || process.env.NEXT_PUBLIC_API_URL_ROOT || ''
|
||||||
|
const plausibleUrl = process.env.NEXT_PUBLIC_PLAUSIBLE_URL || ''
|
||||||
|
const mizikUrl = process.env.NEXT_PUBLIC_OKI_MIZIK_URL || ''
|
||||||
|
const imageDomainsRaw = process.env.NEXT_PUBLIC_DOMAINS_IMAGE || ''
|
||||||
|
|
||||||
|
const apiOrigin = extractOrigin(apiUrl)
|
||||||
|
const plausibleOrigin = extractOrigin(plausibleUrl)
|
||||||
|
const mizikOrigin = extractOrigin(mizikUrl)
|
||||||
|
const imageOrigins = imageDomainsRaw
|
||||||
|
.split(' ')
|
||||||
|
.filter(Boolean)
|
||||||
|
.map(entry => extractHostSource(entry))
|
||||||
|
.filter(Boolean)
|
||||||
|
|
||||||
|
const imageOriginsSet = new Set([apiOrigin, ...imageOrigins])
|
||||||
|
const imageOriginsUnique = [...imageOriginsSet].filter(Boolean)
|
||||||
|
|
||||||
|
const connectSrc = ['\'self\'', apiOrigin, plausibleOrigin, mizikOrigin].filter(Boolean)
|
||||||
|
const imgSrc = ['\'self\'', 'data:', 'blob:', ...imageOriginsUnique].filter(Boolean)
|
||||||
|
const mediaSrc = ['\'self\'', apiOrigin, mizikOrigin].filter(Boolean)
|
||||||
|
|
||||||
|
// Frame-src is intentionally broad: the application embeds user-supplied
|
||||||
|
// PeerTube instances in addition to known platforms (Tidal, Deezer, Spotify,
|
||||||
|
// Soundcloud, Apple Music). Restricting this to a fixed list would break
|
||||||
|
// user-generated content.
|
||||||
|
const frameSrc = ['\'self\'', 'https:']
|
||||||
|
|
||||||
|
const directives = [
|
||||||
|
'default-src \'self\'',
|
||||||
|
`script-src 'nonce-${nonce}' 'strict-dynamic'`,
|
||||||
|
`style-src-elem 'self' 'nonce-${nonce}'`,
|
||||||
|
'style-src-attr \'unsafe-inline\'',
|
||||||
|
`img-src ${imgSrc.join(' ')}`,
|
||||||
|
'font-src \'self\'',
|
||||||
|
`connect-src ${connectSrc.join(' ')}`,
|
||||||
|
`frame-src ${frameSrc.join(' ')}`,
|
||||||
|
`media-src ${mediaSrc.join(' ')}`,
|
||||||
|
'manifest-src \'self\'',
|
||||||
|
'object-src \'none\'',
|
||||||
|
'base-uri \'self\'',
|
||||||
|
'form-action \'self\'',
|
||||||
|
'frame-ancestors \'self\'',
|
||||||
|
'upgrade-insecure-requests'
|
||||||
|
]
|
||||||
|
|
||||||
|
return directives.join('; ')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function proxy(request) {
|
||||||
|
const nonce = generateNonce()
|
||||||
|
const requestHeaders = new Headers(request.headers)
|
||||||
|
requestHeaders.set('x-nonce', nonce)
|
||||||
|
|
||||||
|
const response = NextResponse.next({
|
||||||
|
request: {
|
||||||
|
headers: requestHeaders
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
response.headers.set('Content-Security-Policy', buildCsp(nonce))
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
matcher: [
|
||||||
|
{
|
||||||
|
source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
|
||||||
|
missing: [
|
||||||
|
{type: 'header', key: 'next-router-prefetch'},
|
||||||
|
{type: 'header', key: 'purpose', value: 'prefetch'}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -2356,6 +2356,20 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
tslib "^2.8.0"
|
tslib "^2.8.0"
|
||||||
|
|
||||||
|
"@testing-library/dom@^10.4.1":
|
||||||
|
version "10.4.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-10.4.1.tgz#d444f8a889e9a46e9a3b4f3b88e0fcb3efb6cf95"
|
||||||
|
integrity sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==
|
||||||
|
dependencies:
|
||||||
|
"@babel/code-frame" "^7.10.4"
|
||||||
|
"@babel/runtime" "^7.12.5"
|
||||||
|
"@types/aria-query" "^5.0.1"
|
||||||
|
aria-query "5.3.0"
|
||||||
|
dom-accessibility-api "^0.5.9"
|
||||||
|
lz-string "^1.5.0"
|
||||||
|
picocolors "1.1.1"
|
||||||
|
pretty-format "^27.0.2"
|
||||||
|
|
||||||
"@testing-library/jest-dom@^6.9.1":
|
"@testing-library/jest-dom@^6.9.1":
|
||||||
version "6.9.1"
|
version "6.9.1"
|
||||||
resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz#7613a04e146dd2976d24ddf019730d57a89d56c2"
|
resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz#7613a04e146dd2976d24ddf019730d57a89d56c2"
|
||||||
@@ -2392,6 +2406,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
tslib "^2.4.0"
|
tslib "^2.4.0"
|
||||||
|
|
||||||
|
"@types/aria-query@^5.0.1":
|
||||||
|
version "5.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.4.tgz#1a31c3d378850d2778dabb6374d036dcba4ba708"
|
||||||
|
integrity sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==
|
||||||
|
|
||||||
"@types/chai@^5.2.2":
|
"@types/chai@^5.2.2":
|
||||||
version "5.2.3"
|
version "5.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/@types/chai/-/chai-5.2.3.tgz#8e9cd9e1c3581fa6b341a5aed5588eb285be0b4a"
|
resolved "https://registry.yarnpkg.com/@types/chai/-/chai-5.2.3.tgz#8e9cd9e1c3581fa6b341a5aed5588eb285be0b4a"
|
||||||
@@ -2776,6 +2795,11 @@ ansi-styles@^4.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
color-convert "^2.0.1"
|
color-convert "^2.0.1"
|
||||||
|
|
||||||
|
ansi-styles@^5.0.0:
|
||||||
|
version "5.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b"
|
||||||
|
integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==
|
||||||
|
|
||||||
aproba@^1.0.3:
|
aproba@^1.0.3:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
|
resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
|
||||||
@@ -2794,6 +2818,13 @@ argparse@^2.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
|
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
|
||||||
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
|
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
|
||||||
|
|
||||||
|
aria-query@5.3.0:
|
||||||
|
version "5.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e"
|
||||||
|
integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==
|
||||||
|
dependencies:
|
||||||
|
dequal "^2.0.3"
|
||||||
|
|
||||||
aria-query@^5.0.0:
|
aria-query@^5.0.0:
|
||||||
version "5.3.2"
|
version "5.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.2.tgz#93f81a43480e33a338f19163a3d10a50c01dcd59"
|
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.2.tgz#93f81a43480e33a338f19163a3d10a50c01dcd59"
|
||||||
@@ -3525,6 +3556,11 @@ delegates@^1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
|
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
|
||||||
integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
|
integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
|
||||||
|
|
||||||
|
dequal@^2.0.3:
|
||||||
|
version "2.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
|
||||||
|
integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
|
||||||
|
|
||||||
detect-libc@^1.0.3:
|
detect-libc@^1.0.3:
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
|
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
|
||||||
@@ -3556,6 +3592,11 @@ doctrine@^3.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
esutils "^2.0.2"
|
esutils "^2.0.2"
|
||||||
|
|
||||||
|
dom-accessibility-api@^0.5.9:
|
||||||
|
version "0.5.16"
|
||||||
|
resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453"
|
||||||
|
integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==
|
||||||
|
|
||||||
dom-accessibility-api@^0.6.3:
|
dom-accessibility-api@^0.6.3:
|
||||||
version "0.6.3"
|
version "0.6.3"
|
||||||
resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz#993e925cc1d73f2c662e7d75dd5a5445259a8fd8"
|
resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz#993e925cc1d73f2c662e7d75dd5a5445259a8fd8"
|
||||||
@@ -5581,6 +5622,11 @@ lru-cache@^6.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
yallist "^4.0.0"
|
yallist "^4.0.0"
|
||||||
|
|
||||||
|
lz-string@^1.5.0:
|
||||||
|
version "1.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941"
|
||||||
|
integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==
|
||||||
|
|
||||||
magic-string@^0.25.0, magic-string@^0.25.7:
|
magic-string@^0.25.0, magic-string@^0.25.7:
|
||||||
version "0.25.7"
|
version "0.25.7"
|
||||||
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051"
|
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051"
|
||||||
@@ -6213,16 +6259,16 @@ peek-readable@^4.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/peek-readable/-/peek-readable-4.1.0.tgz#4ece1111bf5c2ad8867c314c81356847e8a62e72"
|
resolved "https://registry.yarnpkg.com/peek-readable/-/peek-readable-4.1.0.tgz#4ece1111bf5c2ad8867c314c81356847e8a62e72"
|
||||||
integrity sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==
|
integrity sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==
|
||||||
|
|
||||||
|
picocolors@1.1.1, picocolors@^1.1.1:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
|
||||||
|
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
|
||||||
|
|
||||||
picocolors@^1.0.0:
|
picocolors@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
|
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
|
||||||
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
|
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
|
||||||
|
|
||||||
picocolors@^1.1.1:
|
|
||||||
version "1.1.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
|
|
||||||
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
|
|
||||||
|
|
||||||
picomatch@^2.2.2:
|
picomatch@^2.2.2:
|
||||||
version "2.3.1"
|
version "2.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
|
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
|
||||||
@@ -6350,6 +6396,15 @@ pretty-bytes@^5.3.0, pretty-bytes@^5.4.1:
|
|||||||
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
|
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
|
||||||
integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==
|
integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==
|
||||||
|
|
||||||
|
pretty-format@^27.0.2:
|
||||||
|
version "27.5.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e"
|
||||||
|
integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==
|
||||||
|
dependencies:
|
||||||
|
ansi-regex "^5.0.1"
|
||||||
|
ansi-styles "^5.0.0"
|
||||||
|
react-is "^17.0.1"
|
||||||
|
|
||||||
process-nextick-args@~2.0.0:
|
process-nextick-args@~2.0.0:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
|
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
|
||||||
@@ -6448,6 +6503,11 @@ react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1:
|
|||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||||
|
|
||||||
|
react-is@^17.0.1:
|
||||||
|
version "17.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
||||||
|
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
|
||||||
|
|
||||||
react-is@^19.0.0, react-is@^19.2.4:
|
react-is@^19.0.0, react-is@^19.2.4:
|
||||||
version "19.2.5"
|
version "19.2.5"
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-19.2.5.tgz#7e7b54143e9313fed787b23fd4295d5a23872ad9"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-19.2.5.tgz#7e7b54143e9313fed787b23fd4295d5a23872ad9"
|
||||||
|
|||||||
Reference in New Issue
Block a user