diff --git a/.env.sample b/.env.sample index 9bac084..114d967 100644 --- a/.env.sample +++ b/.env.sample @@ -4,7 +4,6 @@ NEXT_PUBLIC_API_URL=$API_URL NEXT_PUBLIC_API_URL_ROOT=http://localhost:1337 SITE_URL=http://localhost:3001 NEXT_PUBLIC_READ_TOKEN= -NEXT_PUBLIC_ADMIN_JWT_SECRET= # IDENTITÉ DU SITE (branding) NEXT_PUBLIC_SITE_NAME=PAWÒL-NU. Paroles et traductions. @@ -18,8 +17,8 @@ NEXT_PUBLIC_EXCLUSIVE_ARTIST_LABEL=OKI Exclusif # FUNKWHALE VARIABLE NEXT_PUBLIC_OKI_MIZIK_URL=https://funkwhale-server.com -NEXT_PUBLIC_MIZIK_API_USER=user -NEXT_PUBLIC_MIZIK_API_PASSWORD=password +MIZIK_API_USER=user +MIZIK_API_PASSWORD=password NEXT_PUBLIC_AWTIS_POU_CHAK_PAJ=6 NEXT_PUBLIC_SITE_URL=$SITE_URL @@ -77,7 +76,6 @@ NEXT_PUBLIC_LIBERAPAY_DONATE= NEXT_PUBLIC_PALE_USERNAME= NEXT_PUBLIC_GADE_USERNAME= NEXT_PUBLIC_YOUTUBE_USERNAME= -NEXT_PUBLIC_TELEGRAM_GROUP= NEXT_PUBLIC_XMPP= NEXT_PUBLIC_GIT= NEXT_PUBLIC_CODEBERG= @@ -86,9 +84,6 @@ NEXT_PUBLIC_BLUESKY_URL= # DOMAIN IMAGE NEXT_PUBLIC_DOMAINS_IMAGE="localhost:1337 strapi.mondomaine.com" -# JWT SECRET -NEXT_PUBLIC_JWT_SECRET= - # STRIPE NEXT_PUBLIC_STRIPE_PUBLIC_KEY= diff --git a/.gitea/workflows/check-pr.yml b/.gitea/workflows/check-pr.yml index eab46d2..45e9192 100644 --- a/.gitea/workflows/check-pr.yml +++ b/.gitea/workflows/check-pr.yml @@ -22,6 +22,12 @@ jobs: - name: Vérifier les dépendances run: yarn install --frozen-lockfile + - name: Lancer le lint + run: yarn lint + + - name: Lancer les tests + run: yarn test + deploy-beta: needs: check runs-on: ubuntu-latest diff --git a/.gitea/workflows/deploy-beta.yml b/.gitea/workflows/deploy-beta.yml index 357e9fd..d02ce46 100644 --- a/.gitea/workflows/deploy-beta.yml +++ b/.gitea/workflows/deploy-beta.yml @@ -20,6 +20,12 @@ jobs: - name: Vérifier les dépendances run: yarn install --frozen-lockfile + - name: Lancer le lint + run: yarn lint + + - name: Lancer les tests + run: yarn test + deploy: needs: check runs-on: ubuntu-latest diff --git a/.gitea/workflows/deploy-prod.yml b/.gitea/workflows/deploy-prod.yml index 29297d7..cc42c19 100644 --- a/.gitea/workflows/deploy-prod.yml +++ b/.gitea/workflows/deploy-prod.yml @@ -22,6 +22,12 @@ jobs: - name: Vérifier les dépendances run: yarn install --frozen-lockfile + - name: Lancer le lint + run: yarn lint + + - name: Lancer les tests + run: yarn test + deploy: needs: check runs-on: ubuntu-latest diff --git a/app/__tests__/robots.test.js b/app/__tests__/robots.test.js new file mode 100644 index 0000000..a2e996c --- /dev/null +++ b/app/__tests__/robots.test.js @@ -0,0 +1,22 @@ +import {describe, it, expect, afterEach} from 'vitest' +import robots from '../robots' + +describe('robots', () => { + const originalNodeEnv = process.env.NODE_ENV + + afterEach(() => { + process.env.NODE_ENV = originalNodeEnv + }) + + it('autorise l\'indexation en production', () => { + process.env.NODE_ENV = 'production' + + expect(robots().rules).toEqual({userAgent: '*', allow: '/'}) + }) + + it('bloque l\'indexation hors production', () => { + process.env.NODE_ENV = 'development' + + expect(robots().rules).toEqual({userAgent: '*', disallow: '/'}) + }) +}) diff --git a/app/api/mizik-stream/[id]/__tests__/route.test.js b/app/api/mizik-stream/[id]/__tests__/route.test.js new file mode 100644 index 0000000..a5fdc97 --- /dev/null +++ b/app/api/mizik-stream/[id]/__tests__/route.test.js @@ -0,0 +1,55 @@ +import {describe, it, expect, vi, afterEach} from 'vitest' +import {GET} from '../route' + +function fakeUpstreamResponse({status = 200, headers = {}, body = 'audio-bytes'} = {}) { + return { + status, + body, + headers: { + get: key => headers[key.toLowerCase()] ?? null + } + } +} + +describe('GET /api/mizik-stream/[id]', () => { + const originalFetch = global.fetch + + afterEach(() => { + global.fetch = originalFetch + vi.restoreAllMocks() + }) + + it('n\'expose pas les identifiants au client, uniquement l\'id dans l\'URL amont', async () => { + global.fetch = vi.fn(async () => fakeUpstreamResponse()) + + const request = new Request('http://localhost/api/mizik-stream/42') + await GET(request, {params: Promise.resolve({id: '42'})}) + + const [calledUrl] = global.fetch.mock.calls[0] + expect(calledUrl).toContain('id=42') + expect(calledUrl).toMatch(/[?&]u=/) + expect(calledUrl).toMatch(/[?&]p=/) + }) + + it('transmet l\'en-tête Range pour permettre le seek audio', async () => { + global.fetch = vi.fn(async () => fakeUpstreamResponse({status: 206, headers: {'content-range': 'bytes 0-99/200'}})) + + const request = new Request('http://localhost/api/mizik-stream/42', {headers: {range: 'bytes=0-99'}}) + const response = await GET(request, {params: Promise.resolve({id: '42'})}) + + const [, options] = global.fetch.mock.calls[0] + expect(options.headers.range).toBe('bytes=0-99') + expect(response.status).toBe(206) + expect(response.headers.get('content-range')).toBe('bytes 0-99/200') + }) + + it('renvoie le flux et le statut renvoyés par le serveur Funkwhale', async () => { + global.fetch = vi.fn(async () => fakeUpstreamResponse({status: 200, headers: {'content-type': 'audio/mpeg'}})) + + const request = new Request('http://localhost/api/mizik-stream/7') + const response = await GET(request, {params: Promise.resolve({id: '7'})}) + + expect(response.status).toBe(200) + expect(response.headers.get('content-type')).toBe('audio/mpeg') + }) +}) diff --git a/app/api/mizik-stream/[id]/route.js b/app/api/mizik-stream/[id]/route.js new file mode 100644 index 0000000..c4d5829 --- /dev/null +++ b/app/api/mizik-stream/[id]/route.js @@ -0,0 +1,29 @@ +const MIZIK_URL = process.env.NEXT_PUBLIC_OKI_MIZIK_URL || 'https://funkwhale-server.com' +const MIZIK_API_USER = process.env.MIZIK_API_USER || 'user' +const MIZIK_API_PASSWORD = process.env.MIZIK_API_PASSWORD || 'password' + +const FORWARDED_HEADERS = ['content-type', 'content-length', 'content-range', 'accept-ranges'] + +export async function GET(request, props) { + const {id} = await props.params + + const upstreamUrl = `${MIZIK_URL}/rest/stream?u=${encodeURIComponent(MIZIK_API_USER)}&p=${encodeURIComponent(MIZIK_API_PASSWORD)}&id=${encodeURIComponent(id)}` + const range = request.headers.get('range') + + const upstreamResponse = await fetch(upstreamUrl, { + headers: range ? {range} : {} + }) + + const headers = new Headers() + for (const key of FORWARDED_HEADERS) { + const value = upstreamResponse.headers.get(key) + if (value) { + headers.set(key, value) + } + } + + return new Response(upstreamResponse.body, { + status: upstreamResponse.status, + headers + }) +} diff --git a/app/auth-provider.js b/app/auth-provider.js deleted file mode 100644 index d28f281..0000000 --- a/app/auth-provider.js +++ /dev/null @@ -1,17 +0,0 @@ -'use client' - -import Proptypes from 'prop-types' -import {SessionProvider} from 'next-auth/react' - -export default function AuthProvider({children, session}) { - return ( - - {children} - - ) -} - -AuthProvider.propTypes = { - children: Proptypes.node, - session: Proptypes.object -} diff --git a/app/awtis/[slug]/page.js b/app/awtis/[slug]/page.js index 8df33d5..dc9a08d 100644 --- a/app/awtis/[slug]/page.js +++ b/app/awtis/[slug]/page.js @@ -20,7 +20,7 @@ async function jwennAwtis(slug) { } export async function generateMetadata(props) { - const params = await props.params; + const params = await props.params const {slug} = params const anAwtis = await jwennAwtis(slug) @@ -64,7 +64,7 @@ export async function generateMetadata(props) { } export default async function AwtisPajSlug(props) { - const params = await props.params; + const params = await props.params const {slug} = params const anAwtis = await jwennAwtis(slug) diff --git a/app/awtis/page.js b/app/awtis/page.js index be22ea7..4fb29e1 100644 --- a/app/awtis/page.js +++ b/app/awtis/page.js @@ -63,7 +63,7 @@ async function jwennDone(paj) { } export default async function AwitsPaj(props) { - const searchParams = await props.searchParams; + const searchParams = await props.searchParams const {paj} = searchParams const {pajTotal, awtisPouChakPaj, pajParsed} = await jwennDone(paj || 1) diff --git a/app/layout.js b/app/layout.js index 34a017d..8dcd484 100644 --- a/app/layout.js +++ b/app/layout.js @@ -78,7 +78,7 @@ export default async function RootLayout({children}) { ) return ( - + {plausibleUrl ? {inner} diff --git a/app/page.js b/app/page.js index b8acf37..ce8c101 100644 --- a/app/page.js +++ b/app/page.js @@ -1,5 +1,3 @@ -export const dynamic = 'force-dynamic' - import Box from '@mui/material/Box' import Container from '@mui/material/Container' import {notFound} from 'next/navigation' @@ -9,7 +7,6 @@ import Statistik from '../components/akey/statistik' import Akey from '../components/akey' import AnVedette from '../components/akey/an-vedette' -import okiLogo from '../public/logo-512x512.png' import Footer from '../components/footer' import Aso from '../components/akey/aso' @@ -28,7 +25,7 @@ export default async function Page() { return ( - + {dernierTeks && } diff --git a/app/paroles/[slug]/page.js b/app/paroles/[slug]/page.js index 68f1753..2c58daa 100644 --- a/app/paroles/[slug]/page.js +++ b/app/paroles/[slug]/page.js @@ -3,7 +3,7 @@ import Box from '@mui/material/Box' import {jwennTeksEpiSlug} from '../../../lib/oki-api' import AnTeks from '../../../components/teks/an-teks' -import {getAlias} from '../../../lib/utils/format' +import {getAlias} from '../../../lib/utils/get-alias' import {formatKuveti} from '../../../lib/kuveti' import Footer from '../../../components/footer' @@ -21,14 +21,14 @@ async function jwennAnTeks(slug) { } export async function generateMetadata(props) { - const params = await props.params; + const params = await props.params const {slug} = params - const anTeks = await jwennAnTeks(slug) + const anTeks = await jwennAnTeks(slug) const awtis = anTeks?.artistes?.length === 1 ? anTeks?.artistes[0].alias : getAlias(anTeks.artistes, anTeks.prioriteArtistes) const title = `PAWÒL-NU | ${awtis} - ${anTeks.titre}` - const description = `Paroles de « ${anTeks?.titre} » : ${anTeks?.transcription.slice(0, 100)}...` + const description = `Paroles de « ${anTeks?.titre} » : ${anTeks?.transcription?.slice(0, 100) ?? ''}...` const url = `${siteUrl}/paroles/${slug}` const {couverture} = anTeks @@ -67,12 +67,12 @@ export async function generateMetadata(props) { } export default async function AnPawolPaj(props) { - const params = await props.params; + const params = await props.params const {slug} = params const anTeks = await jwennAnTeks(slug) const {couverture} = anTeks - const teksKuvetiFormat = formatKuveti(couverture) + const teksKuvetiFormat = formatKuveti(couverture) const jsonLd = { '@context': 'http://schema.org', diff --git a/app/paroles/layout.js b/app/paroles/layout.js index 63eccfa..ad89736 100644 --- a/app/paroles/layout.js +++ b/app/paroles/layout.js @@ -1,4 +1,5 @@ import {Suspense} from 'react' +import PropTypes from 'prop-types' import TeksDrawerLoader from '../../components/teks/teks-drawer-loader' import {CurrentTrackProvider} from '../../components/teks/current-track-context' import Loading from './loading' @@ -50,3 +51,7 @@ export default function PawolLayout({children}) { ) } + +PawolLayout.propTypes = { + children: PropTypes.node.isRequired +} diff --git a/app/robots.js b/app/robots.js index c3e7dd2..a029cae 100644 --- a/app/robots.js +++ b/app/robots.js @@ -1,5 +1,5 @@ export default function robots() { - const isProduction = process.env.NEXT_PUBLIC_ENV === 'production'; + const isProduction = process.env.NODE_ENV === 'production' if (!isProduction) { return { diff --git a/app/sipote/page.js b/app/sipote/page.js index 427436f..a427aec 100644 --- a/app/sipote/page.js +++ b/app/sipote/page.js @@ -14,7 +14,7 @@ export default function Sipote() { - + Soutenir ORGANISATION KA INTERNATIONALE ! diff --git a/app/sitemap.js b/app/sitemap.js index 9f0f1f0..f6e6469 100644 --- a/app/sitemap.js +++ b/app/sitemap.js @@ -31,10 +31,6 @@ export default async function sitemap() { { url: `${url}/awtis`, priority: 0.6 - }, - { - url: `${url}/pwopose`, - priority: 0.5 } ] diff --git a/app/theme-registy.js b/app/theme-registy.js index 3730a86..323a247 100644 --- a/app/theme-registy.js +++ b/app/theme-registy.js @@ -69,13 +69,13 @@ export default function ThemeRegistry(props) { return ( <> - - - - - {children} - - + + + + + {children} + + ) } diff --git a/components/akey/an-vedette.js b/components/akey/an-vedette.js index c22d285..ae33d75 100644 --- a/components/akey/an-vedette.js +++ b/components/akey/an-vedette.js @@ -10,7 +10,7 @@ import Chip from '@mui/material/Chip' import Image from 'next/image' import Link from 'next/link' -import {getAlias} from '../../lib/utils/format' +import {getAlias} from '../../lib/utils/get-alias' import {formatKuveti} from '../../lib/kuveti' const IMAGE_URL = process.env.NEXT_PUBLIC_API_URL_ROOT || 'http://localhost:1337' @@ -36,10 +36,10 @@ export default function AnVedette({teks}) { {fmt?.url ? ( {titre} @@ -25,7 +20,3 @@ export default function Akey({logo}) { ) } - -Akey.propTypes = { - logo: PropTypes.object.isRequired -} diff --git a/components/awtis/awtis-detay.js b/components/awtis/awtis-detay.js index 972b674..ab4a419 100644 --- a/components/awtis/awtis-detay.js +++ b/components/awtis/awtis-detay.js @@ -45,11 +45,11 @@ export default function AwtisDetay({anAwtis}) { const hasStreaming = isExclusiveArtist && titrePhare?.streamAudio?.length > 0 const coverUrl = titrePhare?.couverture - ? `${IMAGE_URL}${titrePhare.couverture.formats?.small?.url || titrePhare.couverture.formats?.thumbnail?.url || titrePhare.couverture.url}` + ? `${IMAGE_URL}${formatKuveti(titrePhare.couverture, 'small')?.url}` : null const photoUrl = photo?.url - ? `${IMAGE_URL}${photo.formats?.small?.url || photo.formats?.thumbnail?.url || photo.url}` + ? `${IMAGE_URL}${formatKuveti(photo, 'small')?.url}` : null return ( @@ -72,7 +72,8 @@ export default function AwtisDetay({anAwtis}) { position: 'relative', flexShrink: 0, mb: 2, - }}> + }} + > {photoUrl ? ( 0 && ( - {rezoSosyal.map((rezo, i) => ( - + {rezoSosyal.map(rezo => ( + ))} )} @@ -149,9 +150,9 @@ export default function AwtisDetay({anAwtis}) { {titrePhare.titre} - + {titrePhare.titre} @@ -172,8 +173,8 @@ export default function AwtisDetay({anAwtis}) { Écouter sur - {titrePhare.streamAudio.map((lyen, i) => ( - + {titrePhare.streamAudio.map(lyen => ( + ))} @@ -193,7 +194,7 @@ export default function AwtisDetay({anAwtis}) { {sortedTeks.map(anPawol => { const {couverture} = anPawol - const kuvetiFormat = couverture?.formats?.thumbnail || formatKuveti(couverture, 'small') + const kuvetiFormat = formatKuveti(couverture, 'thumbnail') return ( @@ -202,7 +203,7 @@ export default function AwtisDetay({anAwtis}) { })} - ) : paroles.length === 0 ? ( + ) : (paroles.length === 0 ? ( Aucune parole pour le moment @@ -210,10 +211,10 @@ export default function AwtisDetay({anAwtis}) { Parole - + - )} + ))} diff --git a/components/awtis/awtis-kat.js b/components/awtis/awtis-kat.js index 921e9ba..889335d 100644 --- a/components/awtis/awtis-kat.js +++ b/components/awtis/awtis-kat.js @@ -15,10 +15,10 @@ import Chip from '@mui/material/Chip' import {styled} from '@mui/material/styles' import VerifiedIcon from '@mui/icons-material/Verified' +import {formatKuveti} from '../../lib/kuveti' import AwtisBiyografi from './awtis-biyografi' const PREFIX = 'awtis-kat' -const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3001' const IMAGE_URL = process.env.NEXT_PUBLIC_API_URL_ROOT || 'http://localhost:1337' const EXCLUSIVE_LABEL = process.env.NEXT_PUBLIC_EXCLUSIVE_ARTIST_LABEL || 'OKI Exclusif' @@ -82,8 +82,8 @@ export default function AwtisKat({artiste}) { className={classes.media} component='img' alt={alias} - image={`${photo?.url ? `${IMAGE_URL}${photo?.formats?.thumbnail?.url || photo?.url}` : noImageUrl}`} - loading='lazy' + image={`${photo?.url ? `${IMAGE_URL}${formatKuveti(photo, 'thumbnail')?.url}` : noImageUrl}`} + loading='lazy' title={alias} /> diff --git a/components/awtis/cheche-awtis.js b/components/awtis/cheche-awtis.js index 3d3c897..5cdad9a 100644 --- a/components/awtis/cheche-awtis.js +++ b/components/awtis/cheche-awtis.js @@ -5,10 +5,10 @@ import {useRouter} from 'next/navigation' import TextField from '@mui/material/TextField' import Autocomplete from '@mui/material/Autocomplete' import Avatar from '@mui/material/Avatar' -import CircularProgress from '@mui/material/CircularProgress' import Container from '@mui/material/Container' import {jwennToutAwtis} from '../../lib/oki-api' +import {formatKuveti} from '../../lib/kuveti' const IMAGE_URL = process.env.NEXT_PUBLIC_API_URL_ROOT || 'http://localhost:1337' @@ -28,7 +28,7 @@ export default function ChecheAwtis() { (async () => { try { const {data} = await jwennToutAwtis() - + const filteredData = data.map(artiste => { const firstLetter = artiste.alias[0].toUpperCase() return { @@ -49,15 +49,13 @@ export default function ChecheAwtis() { } }, [loading]) - const sortedOptions = useMemo(() => { - return [...options].sort((a, b) => - -b.firstLetter.localeCompare(a.firstLetter) - ); - }, [options]); + const sortedOptions = useMemo(() => [...options].sort((a, b) => + -b.firstLetter.localeCompare(a.firstLetter) + ), [options]) return ( option?.firstLetter} - getOptionLabel={(option) => option?.alias} - renderOption={(props, option) => { - const {key, ...rest} = props; + groupBy={option => option?.firstLetter} + getOptionLabel={option => option?.alias} + renderOption={(optionProps, option) => { + const {key, ...rest} = optionProps return (
  • {option?.alias}
  • - ); + ) }} - sx={{ width: 300 }} - renderInput={(params) => ( + sx={{width: 300}} + renderInput={params => ( )} onChange={(event, newValue) => { - router.push(`/awtis/${newValue.slug}`); + router.push(`/awtis/${newValue.slug}`) }} onOpen={() => setOpen(true)} onClose={() => setOpen(false)} diff --git a/components/awtis/mizik-lis.js b/components/awtis/mizik-lis.js index cea36e4..c68fb77 100644 --- a/components/awtis/mizik-lis.js +++ b/components/awtis/mizik-lis.js @@ -34,7 +34,7 @@ export default function MizikLis({niAwtis, paroles, meteEsMobilOuve}) { itemContent={index => { const anPawol = pawol[index] const {couverture} = anPawol - const kuvetiFormat = couverture?.formats?.thumbnail || formatKuveti(couverture, 'small') + const kuvetiFormat = formatKuveti(couverture, 'thumbnail') return ( diff --git a/components/awtis/mizik-lyen.js b/components/awtis/mizik-lyen.js index bbd0134..30c4866 100644 --- a/components/awtis/mizik-lyen.js +++ b/components/awtis/mizik-lyen.js @@ -12,7 +12,7 @@ import LibraryMusicIcon from '@mui/icons-material/LibraryMusic' import ExplicitIcon from '@mui/icons-material/Explicit' import {esBrandNew} from '../../lib/date' -import {getAlias} from '../../lib/utils/format' +import {getAlias} from '../../lib/utils/get-alias' const apiUrl = process.env.NEXT_PUBLIC_API_URL_ROOT || 'http://localhost:1337' diff --git a/components/awtis/social-buttons.js b/components/awtis/social-buttons.js index e9ffcd3..dac2588 100644 --- a/components/awtis/social-buttons.js +++ b/components/awtis/social-buttons.js @@ -1,5 +1,6 @@ 'use client' +import PropTypes from 'prop-types' import Chip from '@mui/material/Chip' import IconButton from '@mui/material/IconButton' import Tooltip from '@mui/material/Tooltip' @@ -11,23 +12,23 @@ import { } from '@icons-pack/react-simple-icons' const SOCIAL_CONFIG = { - Mastodon: {label: 'Mastodon', bg: '#6364FF', color: '#fff', Icon: Mastodon}, - Peertube: {label: 'PeerTube', bg: '#F2690D', color: '#fff', Icon: Peertube}, - Pixelfed: {label: 'Pixelfed', bg: '#11D49D', color: '#fff', Icon: null}, - Funkwhale: {label: 'Funkwhale', bg: '#E01B60', color: '#fff', Icon: null}, - Bluesky: {label: 'Bluesky', bg: '#0085FF', color: '#fff', Icon: null}, - Instagram: {label: 'Instagram', bg: '#E4405F', color: '#fff', Icon: Instagram}, - Youtube: {label: 'YouTube', bg: '#FF0000', color: '#fff', Icon: Youtube}, - Tiktok: {label: 'TikTok', bg: '#000000', color: '#fff', Icon: Tiktok}, - Spotify: {label: 'Spotify', bg: '#1DB954', color: '#fff', Icon: Spotify}, - Deezer: {label: 'Deezer', bg: '#EF5466', color: '#fff', Icon: Deezer}, + Mastodon: {label: 'Mastodon', bg: '#6364FF', color: '#fff', Icon: Mastodon}, + Peertube: {label: 'PeerTube', bg: '#F2690D', color: '#fff', Icon: Peertube}, + Pixelfed: {label: 'Pixelfed', bg: '#11D49D', color: '#fff', Icon: null}, + Funkwhale: {label: 'Funkwhale', bg: '#E01B60', color: '#fff', Icon: null}, + Bluesky: {label: 'Bluesky', bg: '#0085FF', color: '#fff', Icon: null}, + Instagram: {label: 'Instagram', bg: '#E4405F', color: '#fff', Icon: Instagram}, + Youtube: {label: 'YouTube', bg: '#FF0000', color: '#fff', Icon: Youtube}, + Tiktok: {label: 'TikTok', bg: '#000000', color: '#fff', Icon: Tiktok}, + Spotify: {label: 'Spotify', bg: '#1DB954', color: '#fff', Icon: Spotify}, + Deezer: {label: 'Deezer', bg: '#EF5466', color: '#fff', Icon: Deezer}, Applemusic: {label: 'Apple Music', bg: '#FC3C44', color: '#fff', Icon: Applemusic}, - Bandcamp: {label: 'Bandcamp', bg: '#1DA0C3', color: '#fff', Icon: Bandcamp}, - Soundcloud: {label: 'SoundCloud', bg: '#FF5500', color: '#fff', Icon: Soundcloud}, - Facebook: {label: 'Facebook', bg: '#1877F2', color: '#fff', Icon: Facebook}, - Twitter: {label: 'X / Twitter', bg: '#000000', color: '#fff', Icon: Twitter}, - Linktree: {label: 'Linktree', bg: '#43E660', color: '#000', Icon: null}, - SiteWeb: {label: 'Site web', bg: '#555555', color: '#fff', Icon: null}, + Bandcamp: {label: 'Bandcamp', bg: '#1DA0C3', color: '#fff', Icon: Bandcamp}, + Soundcloud: {label: 'SoundCloud', bg: '#FF5500', color: '#fff', Icon: Soundcloud}, + Facebook: {label: 'Facebook', bg: '#1877F2', color: '#fff', Icon: Facebook}, + Twitter: {label: 'X / Twitter', bg: '#000000', color: '#fff', Icon: Twitter}, + Linktree: {label: 'Linktree', bg: '#43E660', color: '#000', Icon: null}, + SiteWeb: {label: 'Site web', bg: '#555555', color: '#fff', Icon: null}, } export function SocialButton({rezo}) { @@ -59,15 +60,22 @@ export function SocialButton({rezo}) { return ( ) } + +SocialButton.propTypes = { + rezo: PropTypes.shape({ + plateforme: PropTypes.string.isRequired, + url: PropTypes.string.isRequired + }).isRequired +} diff --git a/components/cc/license-modal.js b/components/cc/license-modal.js index ecb2d6f..1b65503 100644 --- a/components/cc/license-modal.js +++ b/components/cc/license-modal.js @@ -57,7 +57,7 @@ export default function LicenseModal({license, sourceOriginale, remixes}) { {sourceOriginale && ( - + Basé sur @@ -76,7 +76,7 @@ export default function LicenseModal({license, sourceOriginale, remixes}) { )} {remixes && ( - + Déclinaisons {remixes.map(remix => ( diff --git a/components/cgu/index.js b/components/cgu/index.js index ae3422e..faf4d54 100644 --- a/components/cgu/index.js +++ b/components/cgu/index.js @@ -530,7 +530,7 @@ export default function Cgu() { 10. Propriété intellectuelle et licence - + Le site internet pawol.nu et les éléments qui y sont accessibles (textes, images, graphismes, logos, vidéos, icônes, sons, etc.) sont, sauf mention contraire, mis à disposition sous la licence GNU Affero General Public License Version 3 (AGPL-3.0). Cette licence garantit aux utilisateurs les libertés suivantes : diff --git a/components/files/files-list.js b/components/files/files-list.js index a1410ab..6fa63fc 100644 --- a/components/files/files-list.js +++ b/components/files/files-list.js @@ -70,12 +70,14 @@ export default function FilesList({files}) { useEffect(() => { const audioFiles = files.filter(f => f.mime.startsWith('audio')) - if (audioFiles.length === 0) return + if (audioFiles.length === 0) { + return + } let cancelled = false async function fetchAllMeta() { - const mm = await import('music-metadata-browser') + const mm = await import('music-metadata-browser') // eslint-disable-line node/no-unsupported-features/es-syntax const results = {} await Promise.all( audioFiles.map(async file => { @@ -98,7 +100,9 @@ export default function FilesList({files}) { } }) ) - if (!cancelled) setAudioMeta(results) + if (!cancelled) { + setAudioMeta(results) + } } fetchAllMeta() @@ -121,7 +125,8 @@ export default function FilesList({files}) { fontWeight: 'bold', letterSpacing: '0.05rem', textTransform: 'uppercase', - }}>{isHaute ? 'Haute' : 'Faible'} + }} + >{isHaute ? 'Haute' : 'Faible'} ) } @@ -150,12 +155,16 @@ export default function FilesList({files}) { } useEffect(() => () => { - Object.values(controllersRef.current).forEach(c => c.abort()) + for (const c of Object.values(controllersRef.current)) { + c.abort() + } }, []) const handleClick = async (e, url, fileName, fileId) => { e.stopPropagation() - if (fileId in downloading) return + if (fileId in downloading) { + return + } const controller = new AbortController() controllersRef.current[fileId] = controller @@ -163,14 +172,18 @@ export default function FilesList({files}) { setDownloading(prev => ({...prev, [fileId]: 0})) try { const response = await fetch(url, {signal: controller.signal}) - const contentLength = +response.headers.get('content-length') + const contentLength = Number(response.headers.get('content-length')) const reader = response.body.getReader() const chunks = [] let received = 0 - while (true) { + for (;;) { + // eslint-disable-next-line no-await-in-loop -- chaque chunk dépend du précédent, la lecture est intrinsèquement séquentielle const {done, value} = await reader.read() - if (done) break + if (done) { + break + } + chunks.push(value) received += value.length if (contentLength) { @@ -186,7 +199,9 @@ export default function FilesList({files}) { a.click() URL.revokeObjectURL(blobUrl) } catch (error) { - if (error.name !== 'AbortError') throw error + if (error.name !== 'AbortError') { + throw error + } } finally { delete controllersRef.current[fileId] setDownloading(prev => { diff --git a/components/files/kits-dialog.js b/components/files/kits-dialog.js index 78964b2..8d6ab21 100644 --- a/components/files/kits-dialog.js +++ b/components/files/kits-dialog.js @@ -48,10 +48,10 @@ export default function KitsDialog({kits}) { Télécharger un kit - {kits.map((kit, index) => { + {kits.map(kit => { const PlatformIcon = PLATFORM_ICON[kit.plateforme] ?? CloudDownloadIcon return ( - + diff --git a/components/komante/ekri-komante.js b/components/komante/ekri-komante.js deleted file mode 100644 index 34d8338..0000000 --- a/components/komante/ekri-komante.js +++ /dev/null @@ -1,156 +0,0 @@ -import {useCallback, useEffect, useState, forwardRef} from 'react' -import PropTypes from 'prop-types' -import axios from 'axios' -import { - TextField, - Container, - Button, - Snackbar, - LinearProgress, - Typography -} from '@mui/material' -import MuiAlert from '@mui/material/Alert' - -const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:1337/api' - -const Alert = forwardRef(function Alert(props, ref) { - return -}) -function EkriKomante({session, paroleId, meteEsKomanteOuve}) { - const {jwt, user} = session - const [komante, meteKomante] = useState({kontni: ''}) - const [ere, meteEre] = useState('') - const [sikse, meteSikse] = useState('') - const [chaje, meteChaje] = useState(false) - const [esOuve, meteEsOuve] = useState(false) - - const handleClick = async () => { - meteChaje(true) - const {kontni} = komante - - if (kontni === '') { - meteEre({error: {message: 'Champ obligatoire'}}) - meteChaje(false) - - return - } - - const headers = { - 'content-type': 'application/json', - Authorization: `Bearer ${jwt}` - } - - try { - await axios.post(`${API_URL}/commentaires`, { - data: { - contenu: kontni, - parole: paroleId, - user: { - id: user.id, - username: user.username, - email: user.email - }, - datePublication: new Date() - } - }, { - headers - }) - - meteSikse('Commentaire envoyé avec succès. Il apparaîtra sur le site après validation.') - meteChaje(false) - } catch (error) { - meteEre(error?.response?.data) - meteChaje(false) - } - } - - const handleUpdate = useCallback(update => { - meteKomante({...komante, ...update}) - }, [komante]) - - const handleClose = (event, reason) => { - if (reason === 'clickaway') { - return - } - - meteEsOuve(false) - meteSikse('') - meteEre('') - meteEsKomanteOuve(false) - } - - const handleReset = () => { - meteKomante({kontni: ''}) - } - - useEffect(() => { - if (sikse) { - meteEsOuve(true) - handleReset() - } - }, [sikse]) - - useEffect(() => { - if (ere) { - meteEsOuve(true) - } - }, [ere]) - - return ( - -
    - handleUpdate({kontni: event.target.value})} - /> - -
    - - {chaje && } -
    - {sikse && ( - - - {sikse} - - - )} - {ere && ( - - - Une erreur s’est produite : {ere?.error?.message} - - - )} -
    - ) -} - -EkriKomante.propTypes = { - session: PropTypes.object.isRequired, - paroleId: PropTypes.number.isRequired, - meteEsKomanteOuve: PropTypes.func.isRequired -} - -export default EkriKomante - diff --git a/components/komante/komante-list.js b/components/komante/komante-list.js deleted file mode 100644 index f1ad97a..0000000 --- a/components/komante/komante-list.js +++ /dev/null @@ -1,67 +0,0 @@ -import PropTypes from 'prop-types' -import {format} from 'date-fns' -import {fr} from 'date-fns/locale' -import {styled} from '@mui/material/styles' - -import { - Typography, - Divider, - List, - ListItemText -} from '@mui/material' - -import {formatJsonString} from '../../lib/utils/format' - -const PREFIX = 'komante-list' - -const classes = { - root: `${PREFIX}-root`, - inline: `${PREFIX}-inline` -} - -const StyledList = styled(List)(( - { - theme - } -) => ({ - [`&.${classes.root}`]: { - width: '100%', - maxWidth: '36ch', - backgroundColor: theme.palette.background.paper - }, - - [`& .${classes.inline}`]: { - display: 'inline' - } -})) - -export default function KomanteList({commentaires}) { - return ( - - {commentaires.map(({id, user, contenu, datePublication}) => ( -
    - - {user.username} - - } - /> - - {format(new Date(datePublication), 'Pp', {locale: fr})} - - } - /> - -
    - ))} -
    - ) -} - -KomanteList.propTypes = { - commentaires: PropTypes.array.isRequired -} diff --git a/components/komante/vwe-komante.js b/components/komante/vwe-komante.js deleted file mode 100644 index 89fd593..0000000 --- a/components/komante/vwe-komante.js +++ /dev/null @@ -1,136 +0,0 @@ -import {useState, useEffect, useRef} from 'react' -import {styled} from '@mui/material/styles' -import PropTypes from 'prop-types' -import {useSession} from 'next-auth/react' - -import { - Dialog, - DialogTitle, - DialogContent, - DialogActions, - Button, - Typography, - Badge -} from '@mui/material' -import {useRouter} from 'next/navigation' -import Koneksyon from '../sesyon/koneksyon' -import KomanteList from './komante-list' -import EkriKomante from './ekri-komante' - -const PREFIX = 'vwe-komante' - -const classes = { - tooltip: `${PREFIX}-tooltip`, - margin: `${PREFIX}-margin`, - extendedIcon: `${PREFIX}-extendedIcon` -} - -const Root = styled('div')(( - { - theme - } -) => ({ - [`& .${classes.margin}`]: { - margin: theme.spacing(1) - }, - - [`& .${classes.extendedIcon}`]: { - marginRight: theme.spacing(1) - } -})) - -export default function VweKomante({commentaires, parole, paroleId}) { - const [esOuve, meteEsOuve] = useState(false) - const [esKoneksyonOuve, meteEsKoneksyonOuve] = useState(false) - const [esKomenteOuve, meteEsKomanteOuve] = useState(false) - const {data: session} = useSession() - const router = useRouter() - - const handleClick = () => { - meteEsOuve(true) - } - - const handleClose = () => { - meteEsOuve(false) - } - - const descriptionElementRef = useRef(null) - useEffect(() => { - if (esOuve) { - const {current: descriptionElement} = descriptionElementRef - if (descriptionElement !== null) { - descriptionElement.focus() - } - } - }, [esOuve]) - - return ( - - - - - - Commentaires - - - {commentaires.length > 0 ? ( - - ) : ( - - Aucun commentaire - - )} - - - - {session && session.user && !esKomenteOuve && ( - - )} - {!session && !esKoneksyonOuve && ( - - )} - {!session && esKoneksyonOuve && ( - - )} - - {!session && esKoneksyonOuve && ( - - )} - {session && session.user && esKomenteOuve && ( - <> - - - - )} - - - ) -} - -VweKomante.propTypes = { - commentaires: PropTypes.array, - parole: PropTypes.object.isRequired, - paroleId: PropTypes.number.isRequired -} diff --git a/components/navigasyon.js b/components/navigasyon.js index f832ef8..0f19bda 100644 --- a/components/navigasyon.js +++ b/components/navigasyon.js @@ -1,15 +1,11 @@ 'use client' -import {useState} from 'react' -import PropTypes from 'prop-types' import {useRouter, usePathname} from 'next/navigation' import {styled} from '@mui/material/styles' import AppBar from '@mui/material/AppBar' import Tabs, {tabsClasses} from '@mui/material/Tabs' import Tab from '@mui/material/Tab' -import Typography from '@mui/material/Typography' -import Box from '@mui/material/Box' import {useMediaQuery} from '@mui/material' import MusicNoteIcon from '@mui/icons-material/MusicNote' @@ -49,32 +45,6 @@ const tabPath = { awtis: 3, } -function TabPanel(props) { - const {children, value, index, ...other} = props - - return ( - - ) -} - -TabPanel.propTypes = { - children: PropTypes.node, - index: PropTypes.any.isRequired, - value: PropTypes.any.isRequired -} - function a11yProps(index) { return { id: `scrollable-force-tab-${index}`, @@ -89,11 +59,8 @@ export default function Navigasyon() { const selectedTab = tabPath[selectedPath] const isMobile = useMediaQuery('(max-width:422px)') - const [value, setValue] = useState(0) const handleChange = (event, newValue) => { - setValue(newValue) - if (newValue !== selectedTab) { router.push(tabRouteHref[newValue]) } @@ -103,7 +70,8 @@ export default function Navigasyon() {
    - {selectedTab !== undefined && } aria-label='Paroles' {...a11yProps(2)} /> } aria-label='Artistes' {...a11yProps(3)} /> - } + )} - - - -
    ) diff --git a/components/password/new-password.js b/components/password/new-password.js deleted file mode 100644 index a1783e5..0000000 --- a/components/password/new-password.js +++ /dev/null @@ -1,202 +0,0 @@ -import {useState, forwardRef} from 'react' -import PropTypes from 'prop-types' -import axios from 'axios' -import {useRouter} from 'next/navigation' - -import {FormControl, Snackbar, IconButton, Button, Input, InputAdornment, InputLabel, Box, Container, Typography, LinearProgress} from '@mui/material' -import MuiAlert from '@mui/material/Alert' -import Visibility from '@mui/icons-material/Visibility' -import VisibilityOff from '@mui/icons-material/VisibilityOff' -import Link from '@mui/material/Link' - -const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:1337' - -const Alert = forwardRef(function Alert(props, ref) { - return -}) - -export default function NewPassword({code}) { - const router = useRouter() - const [showPassword, setShowPassword] = useState('') - const [showPasswordConfirmation, setShowPasswordConfirmation] = useState('') - const [password, setPassword] = useState('') - const [passwordConfirmation, setPasswordConfirmation] = useState('') - const [loading, setLoading] = useState(false) - const [error, setError] = useState('') - const [open, setOpen] = useState(true) - const [passwordSuccess, setPasswordSuccess] = useState(false) - - const resetForm = () => { - setPassword('') - setPasswordConfirmation('') - } - - const changePassword = async () => { - setLoading(true) - try { - await axios.post(`${API_URL}/auth/reset-password`, { - code, - password, - passwordConfirmation - }) - setLoading(false) - setPasswordSuccess(true) - setTimeout(() => { - router.push('/pwopose') - }, 10_000) - } catch { - setOpen(true) - setError('Une erreur s’est produite. Veuillez réessayer ultérieurement') - } - } - - const handleMouseDownPassword = event => { - event.preventDefault() - } - - const handleMouseDownPasswordConfirmation = event => { - event.preventDefault() - } - - const handleKeyUpPassword = event => { - if (event.keyCode === 13) { - handleClick() - } - } - - const handleKeyUpPasswordConfirmation = event => { - if (event.keyCode === 13) { - handleClick() - } - } - - const handleClick = async () => { - if (password !== passwordConfirmation) { - setOpen(true) - setError('Les 2 mots de passe de correspondent pas') - return - } - - if (password.length < 6) { - setOpen(true) - setError('Le mot de passe est trop court, 6 caratères minimum') - return - } - - await changePassword() - setLoading(false) - resetForm() - } - - const handleClose = (event, reason) => { - if (reason === 'clickaway') { - return - } - - setOpen(false) - setError('') - } - - return ( - - - Nouveau mot de passe - - Mot de passe - - setShowPassword(!showPassword)} - onMouseDown={handleMouseDownPassword} - > - {showPassword ? : } - - - } - onChange={event => setPassword(event.target.value)} - onKeyUp={handleKeyUpPassword} - /> - - - Vérification du mot de passe - - setShowPasswordConfirmation(!showPassword)} - onMouseDown={handleMouseDownPasswordConfirmation} - > - {showPasswordConfirmation ? : } - - - } - onChange={event => setPasswordConfirmation(event.target.value)} - onKeyUp={handleKeyUpPasswordConfirmation} - /> - - - - - - {loading && } - - {error && ( - - {error} - - )} - - {passwordSuccess && ( - - Votre changement de mot de passe a été pris en compte. Vous serez redirigé vers la page de connexion. Cliquez ici si vous n’êtes pas redirigé vers la page de connexion - - )} - - ) -} - -NewPassword.propTypes = { - code: PropTypes.string.isRequired -} diff --git a/components/password/reset-dialog.js b/components/password/reset-dialog.js deleted file mode 100644 index 1e64005..0000000 --- a/components/password/reset-dialog.js +++ /dev/null @@ -1,88 +0,0 @@ -import {useState} from 'react' -import PropTypes from 'prop-types' -import Button from '@mui/material/Button' -import TextField from '@mui/material/TextField' -import Dialog from '@mui/material/Dialog' -import DialogActions from '@mui/material/DialogActions' -import DialogContent from '@mui/material/DialogContent' -import DialogContentText from '@mui/material/DialogContentText' -import DialogTitle from '@mui/material/DialogTitle' - -import {validateEmail} from '../../lib/utils/emails' -import {jwennUserEpiEmail, passwordRequest} from '../../lib/oki-api' - -export default function ResetDialog({lyen, title, activation, content, open, setOpen, setLoading, setError, setSuccess}) { - const [email, setEmail] = useState('') - - const forgotPasswordRequest = async () => { - setLoading(true) - try { - await passwordRequest(lyen, email) - - if (activation) { - const user = await jwennUserEpiEmail(email) - localStorage.setItem('user-id', user?.id) - } - - setLoading(false) - setSuccess(true) - } catch { - setError('Une erreur s’est produite. Veuillez réessayer ultérieurement') - setLoading(false) - } - } - - const resetFrom = () => { - setEmail('') - } - - const handleClick = async () => { - forgotPasswordRequest() - setOpen(false) - resetFrom() - } - - const handleClose = () => { - setOpen(false) - } - - return ( -
    - - {title} - - - {content} - - setEmail(event.target.value)} - /> - - - - - - -
    - ) -} - -ResetDialog.propTypes = { - lyen: PropTypes.string.isRequired, - title: PropTypes.string.isRequired, - activation: PropTypes.bool, - content: PropTypes.string.isRequired, - open: PropTypes.bool.isRequired, - setOpen: PropTypes.func.isRequired, - setLoading: PropTypes.func.isRequired, - setError: PropTypes.func.isRequired, - setSuccess: PropTypes.func.isRequired, -} diff --git a/components/password/reset-password.js b/components/password/reset-password.js deleted file mode 100644 index 6a1908a..0000000 --- a/components/password/reset-password.js +++ /dev/null @@ -1,75 +0,0 @@ -import {useState, forwardRef} from 'react' -import Button from '@mui/material/Button' -import Typography from '@mui/material/Typography' -import Box from '@mui/material/Box' -import Snackbar from '@mui/material/Snackbar' -import {LinearProgress} from '@mui/material' -import MuiAlert from '@mui/material/Alert' -import ResetDialog from './reset-dialog' - -const Alert = forwardRef(function Alert(props, ref) { - return -}) - -export default function ResetPassword() { - const [open, setOpen] = useState(false) - const [error, setError] = useState(false) - const [loading, setLoading] = useState(false) - const [success, setSuccess] = useState(false) - - const handleClose = (event, reason) => { - if (reason === 'clickaway') { - return - } - - setError('') - setSuccess(false) - } - - return ( - <> - - - - {loading && } - {error && ( - - {error} - - )} - {success && ( - - Email envoyé avec succès. Vous pouvez changer votre mot de passe via le lien qui vous a été envoyé. - - )} - - - ) -} diff --git a/components/pwopose.js b/components/pwopose.js deleted file mode 100644 index 6bf2d9c..0000000 --- a/components/pwopose.js +++ /dev/null @@ -1,113 +0,0 @@ -'use client' - -import {useState, useEffect, forwardRef} from 'react' -import PropTypes from 'prop-types' -import {useSession} from 'next-auth/react' -import MuiAlert from '@mui/material/Alert' -import Snackbar from '@mui/material/Snackbar' -import Box from '@mui/material/Box' - -import Koneksyon from '../../components/sesyon/koneksyon' -import Dekoneksyon from '../../components/sesyon/dekoneksyon' -import EkriTeks from '../../components/soumet/ekri-teks' -import Footer from '../../components/footer' - -import {jwennUserEpiToken, jwennUserEpiUsername} from '../../lib/oki-api' -import NewPassword from '../../components/password/new-password' -import ChwaTeks from '../../components/soumet/chwa-teks' - -const Alert = forwardRef(function Alert(props, ref) { - return -}) - -export default function Pwopose({code}) { - const {data: session} = useSession() - const [localUsername, setLocalUsername] = useState(null) - const [username, setUsername] = useState(null) - const [open, setOpen] = useState(true) - const [selectedTeks, setSelectedTeks] = useState(null) - const [canAutoTranslate, setCanAutoTranslate] = useState(false) - - const handleClose = (event, reason) => { - if (reason === 'clickaway') { - return - } - - setOpen(false) - } - - useEffect(() => { - if (session?.jwt) { - const getUser = async token => { - const user = await jwennUserEpiToken(token) - setCanAutoTranslate(user.canAutoTranslate) - } - - getUser(session.jwt) - } - }) - - useEffect(() => { - if (localStorage.getItem('username')) { - const username = localStorage.getItem('username') - setLocalUsername(username) - } - }, []) - - useEffect(() => { - if (localUsername) { - const getUser = async username => { - const user = await jwennUserEpiUsername(username) - setUsername(user?.username) - } - - getUser(localUsername) - } - }, [localUsername]) - - useEffect(() => { - if (username && localStorage.getItem('username')) { - localStorage.removeItem('username') - } - }, [username]) - - return ( - - - {!session && !code && ( - - )} - - {!session && code && ( - - )} - {session && session.user && ( - <> - - - - - )} - {session && !session.user && ( - - )} - {username && ( - - Bonjour {username}, votre compte a été activé avec succès. Vous pouvez vous connecter. - - )} - -