Compare commits

..

52 Commits

Author SHA1 Message Date
cedric 0040902151 fix: typo
Déploiement FRONT PROD / check (push) Successful in 2m9s
Déploiement FRONT PROD / deploy (push) Successful in 21s
2026-06-25 06:18:46 +04:00
cedric 3133145fd9 feat: improve JSON-LD
Déploiement FRONT PROD / check (push) Successful in 2m6s
Déploiement FRONT PROD / deploy (push) Successful in 21s
2026-06-23 19:30:02 +04:00
cedric 35e1a3a010 fix: typo StreamButton
Déploiement FRONT PROD / check (push) Successful in 2m8s
Déploiement FRONT PROD / deploy (push) Successful in 21s
2026-06-22 18:52:14 +04:00
cedric 9949efd0c1 feat: otimize images on awtis-detay 2026-06-22 18:51:57 +04:00
cedric 972e41d528 feat: optimize paroles images 2026-06-22 18:48:26 +04:00
cedric 2d83b878fe fix: typo
Déploiement FRONT PROD / check (push) Successful in 2m4s
Déploiement FRONT PROD / deploy (push) Successful in 21s
2026-06-22 18:44:31 +04:00
cedric eba16e7ec8 feat: add blur to index image
Déploiement FRONT PROD / check (push) Successful in 2m5s
Déploiement FRONT PROD / deploy (push) Successful in 21s
2026-06-22 14:52:07 +04:00
cedric 6121a2ca4a fix: rename StreamButton to StreamButtonComponent
Déploiement FRONT PROD / check (push) Successful in 2m7s
Déploiement FRONT PROD / deploy (push) Successful in 22s
2026-06-22 13:14:33 +04:00
cedric 5a6273e9e3 feat: display stream links on teks
Déploiement FRONT PROD / check (push) Successful in 2m11s
Déploiement FRONT PROD / deploy (push) Successful in 18s
2026-06-22 12:56:29 +04:00
cedric ac24abb051 feat: change priority image for index
Déploiement FRONT PROD / check (push) Successful in 2m2s
Déploiement FRONT PROD / deploy (push) Successful in 21s
2026-06-22 10:13:02 +04:00
cedric 9d1c51337c feat: optimize card display
Déploiement FRONT PROD / check (push) Successful in 2m6s
Déploiement FRONT PROD / deploy (push) Successful in 22s
2026-06-22 08:57:24 +04:00
cedric 4d751482c2 feat: display titrePhare on OKI artist
Déploiement FRONT PROD / check (push) Successful in 2m1s
Déploiement FRONT PROD / deploy (push) Successful in 21s
2026-06-20 18:37:43 +04:00
cedric 45d1891df6 feat: add more informations for isOKIAwtis
Déploiement FRONT PROD / check (push) Successful in 2m12s
Déploiement FRONT PROD / deploy (push) Successful in 22s
2026-06-20 06:09:24 +04:00
cedric e7bf523f75 feat: add desktopUrl
Déploiement FRONT PROD / check (push) Successful in 2m1s
Déploiement FRONT PROD / deploy (push) Successful in 23s
2026-06-18 00:33:28 +04:00
cedric d74691b2a5 feat: improve karaoke feature
Déploiement FRONT PROD / check (push) Successful in 2m6s
Déploiement FRONT PROD / deploy (push) Successful in 21s
2026-06-18 00:14:34 +04:00
cedric 0c339e7201 fix: improve karaoke modal 2026-06-18 00:08:24 +04:00
cedric b12952f85e feat: add karaoke modal
Déploiement FRONT PROD / check (push) Successful in 2m4s
Déploiement FRONT PROD / deploy (push) Successful in 22s
2026-06-17 23:47:14 +04:00
cedric f28fa96f89 fix: declare page dynamic
Déploiement FRONT PROD / check (push) Successful in 2m14s
Déploiement FRONT PROD / deploy (push) Successful in 21s
2026-06-17 09:20:11 +04:00
cedric ed819e3537 feat: add jwennAnVedette
Déploiement FRONT PROD / check (push) Successful in 2m2s
Déploiement FRONT PROD / deploy (push) Successful in 20s
2026-06-17 08:51:06 +04:00
cedric a89196909e fix: screen fixed for WebKit
Déploiement FRONT PROD / check (push) Successful in 2m11s
Déploiement FRONT PROD / deploy (push) Successful in 21s
2026-06-16 13:13:47 +04:00
cedric d80a77d9e7 fix: change modal title color on teks
Déploiement FRONT PROD / check (push) Successful in 2m4s
Déploiement FRONT PROD / deploy (push) Successful in 21s
2026-06-15 21:31:45 +04:00
cedric 2f46b6372f feat: add sourceOriginale to CC modal
Déploiement FRONT PROD / check (push) Successful in 2m4s
Déploiement FRONT PROD / deploy (push) Successful in 22s
2026-06-12 14:14:45 +04:00
cedric ad496f50b0 feat: replace flags by text 2026-06-12 14:14:29 +04:00
cedric 17ef7f5534 typo: NEW RELEASE
Déploiement FRONT PROD / check (push) Successful in 2m3s
Déploiement FRONT PROD / deploy (push) Successful in 22s
2026-06-11 21:17:17 +04:00
cedric 9c18e52046 fix: improve Plausible
Déploiement FRONT PROD / check (push) Successful in 2m13s
Déploiement FRONT PROD / deploy (push) Successful in 21s
2026-06-11 18:52:37 +04:00
cedric 0b50c9ac2f fix: change plausible implementation
Déploiement FRONT PROD / check (push) Successful in 2m10s
Déploiement FRONT PROD / deploy (push) Successful in 21s
2026-06-11 18:45:56 +04:00
cedric d2b779f8de feat: config Plausible
Déploiement FRONT PROD / check (push) Successful in 2m7s
Déploiement FRONT PROD / deploy (push) Successful in 46s
2026-06-11 18:33:42 +04:00
cedric 9adc045a98 chore: add next-plausible lib 2026-06-11 18:33:31 +04:00
cedric f500b2eaa1 fix: change labola repo 2026-06-11 17:56:23 +04:00
cedric b018c2a917 feat: change language order 2026-06-11 17:56:01 +04:00
cedric ca9ba4cb10 feat: add AnVedette component
Déploiement FRONT PROD / check (push) Successful in 2m5s
Déploiement FRONT PROD / deploy (push) Successful in 21s
2026-06-08 21:46:18 +04:00
cedric c113a2547f fix: typo
Déploiement FRONT PROD / check (push) Successful in 2m14s
Déploiement FRONT PROD / deploy (push) Successful in 21s
2026-06-08 01:39:25 +04:00
cedric 6b54f13b3f feat: add image to paroles route 2026-06-08 01:37:59 +04:00
cedric 486a852195 feat: optimize cover in search
Déploiement FRONT PROD / check (push) Successful in 2m0s
Déploiement FRONT PROD / deploy (push) Successful in 21s
2026-06-08 01:25:58 +04:00
cedric a51744e941 feat: add cover to teks page
Déploiement FRONT PROD / check (push) Successful in 2m6s
Déploiement FRONT PROD / deploy (push) Successful in 23s
2026-06-08 01:19:30 +04:00
cedric 13d60a1b32 feat: optimize covers display
Déploiement FRONT PROD / check (push) Successful in 2m7s
Déploiement FRONT PROD / deploy (push) Successful in 30s
2026-06-08 00:52:23 +04:00
cedric 4955327334 fix: display audio length & reset curretTime at the end
Déploiement FRONT PROD / check (push) Successful in 2m19s
Déploiement FRONT PROD / deploy (push) Successful in 22s
2026-06-02 13:36:41 +04:00
cedric 57eeffc8f7 fix: replace publishedAt by createdAt in TeksKat
Déploiement FRONT PROD / check (push) Successful in 2m12s
Déploiement FRONT PROD / deploy (push) Successful in 22s
2026-06-02 09:57:10 +04:00
cedric a09b836218 fix: change Range audio files
Déploiement FRONT PROD / check (push) Successful in 2m6s
Déploiement FRONT PROD / deploy (push) Successful in 22s
2026-06-02 09:47:30 +04:00
cedric 38deb84be8 fix: change theme mode
Déploiement FRONT PROD / check (push) Successful in 2m2s
Déploiement FRONT PROD / deploy (push) Successful in 20s
2026-06-02 09:12:15 +04:00
cedric 2dc0e7933c feat: add pawol to player
Déploiement FRONT PROD / check (push) Successful in 2m9s
Déploiement FRONT PROD / deploy (push) Successful in 22s
2026-06-02 02:55:16 +04:00
cedric 0e2bcb17b2 feat: improve download
Déploiement FRONT PROD / check (push) Successful in 2m0s
Déploiement FRONT PROD / deploy (push) Successful in 21s
2026-06-01 01:19:11 +04:00
cedric cd3cc2a697 fix: explicit download
Déploiement FRONT PROD / check (push) Successful in 2m10s
Déploiement FRONT PROD / deploy (push) Successful in 21s
2026-06-01 00:52:41 +04:00
cedric 7161070435 feat: add cache to audio meta 2026-06-01 00:49:49 +04:00
cedric cb37979fc4 fix: check file info without download all file
Déploiement FRONT PROD / check (push) Successful in 2m0s
Déploiement FRONT PROD / deploy (push) Successful in 20s
2026-06-01 00:28:11 +04:00
cedric 9f8e60d56f feat: improve files info
Déploiement FRONT PROD / check (push) Successful in 2m11s
Déploiement FRONT PROD / deploy (push) Successful in 47s
2026-06-01 00:19:53 +04:00
cedric d02d946cfb remove GADE from rezo lis
Déploiement FRONT PROD / check (push) Successful in 1m58s
Déploiement FRONT PROD / deploy (push) Successful in 20s
2026-05-18 18:57:23 +04:00
cedric eef02431b8 update package metadata
Déploiement FRONT PROD / check (push) Successful in 2m6s
Déploiement FRONT PROD / deploy (push) Successful in 20s
2026-05-18 18:32:15 +04:00
cedric e05c61f0fe update git links to labola 2026-05-18 18:32:15 +04:00
cedric 67aa60a53b switch social link from codeberg to gitea 2026-05-18 18:32:15 +04:00
cedric 860bfe74df add gitea icon 2026-05-18 18:32:15 +04:00
cedric 921318f92f Merge pull request 'CI/CD & ajustements - Mise en place des workflows Gitea Actions' (#1) from dev into master
Déploiement FRONT PROD / check (push) Successful in 2m2s
Déploiement FRONT PROD / deploy (push) Successful in 25s
Reviewed-on: #1
2026-05-16 08:17:47 +00:00
29 changed files with 1099 additions and 243 deletions
+1 -1
View File
@@ -61,7 +61,7 @@ NEXT_PUBLIC_GADE_USERNAME=
NEXT_PUBLIC_YOUTUBE_USERNAME= NEXT_PUBLIC_YOUTUBE_USERNAME=
NEXT_PUBLIC_TELEGRAM_GROUP= NEXT_PUBLIC_TELEGRAM_GROUP=
NEXT_PUBLIC_XMPP= NEXT_PUBLIC_XMPP=
NEXT_PUBLIC_CODEBERG= NEXT_PUBLIC_GIT=
# DOMAIN IMAGE # DOMAIN IMAGE
NEXT_PUBLIC_DOMAINS_IMAGE="localhost:1337 strapi.mondomaine.com" NEXT_PUBLIC_DOMAINS_IMAGE="localhost:1337 strapi.mondomaine.com"
+1 -1
View File
@@ -10,7 +10,7 @@
## Prérequis ## Prérequis
- Node >= 20 - Node >= 20
- [API](https://codeberg.org/OKI/api.pawol.nu) - [API](https://labola.o-k-i.net/ORGANISATION-KA-INTERNATIONALE/api.pawol.nu)
## Variables d'environement ## Variables d'environement
- Copier le contenu du fichier `.env.sample` dans un nouveau fichier `.env` - Copier le contenu du fichier `.env.sample` dans un nouveau fichier `.env`
+1 -1
View File
@@ -24,7 +24,7 @@ export async function generateMetadata(props) {
const {slug} = params const {slug} = params
const anAwtis = await jwennAwtis(slug) const anAwtis = await jwennAwtis(slug)
const title = `OKI | ${anAwtis.alias} - Paroles et Traductions` const title = `PAWÒL-NU | ${anAwtis.alias}`
const description = `${anAwtis.alias}${anAwtis?.biographie ? ` : ${anAwtis?.biographie.slice(0, 100)}...` : ''}` const description = `${anAwtis.alias}${anAwtis?.biographie ? ` : ${anAwtis?.biographie.slice(0, 100)}...` : ''}`
const url = `${siteUrl}/awtis/${slug}` const url = `${siteUrl}/awtis/${slug}`
+3 -3
View File
@@ -11,10 +11,10 @@ import {jwennAwtisPajinasyon} from '../../lib/oki-api'
import Footer from '../../components/footer' import Footer from '../../components/footer'
export const metadata = { export const metadata = {
title: 'OKI | Awtis - Liste des artistes', title: 'PAWÒL-NU | Artistes',
description: 'Liste des artistes ayant une ou plusieurs œuvres présentes sur le site.', description: 'Liste des artistes ayant une ou plusieurs œuvres présentes sur le site.',
openGraph: { openGraph: {
title: 'OKI | Awtis - Liste des artistes', title: 'PAWÒL-NU | Artistes',
description: 'Liste des artistes ayant une ou plusieurs œuvres présentes sur le site.', description: 'Liste des artistes ayant une ou plusieurs œuvres présentes sur le site.',
url: 'https://pawol.nu/sipote', url: 'https://pawol.nu/sipote',
siteName: 'PAWÒL-NU. Paroles et traductions.', siteName: 'PAWÒL-NU. Paroles et traductions.',
@@ -31,7 +31,7 @@ export const metadata = {
twitter: { twitter: {
site: '@OrganisationKA', site: '@OrganisationKA',
card: 'summary_large_image', card: 'summary_large_image',
title: 'OKI | Awtis - Liste des artistes', title: 'PAWÒL-NU | Artistes',
description: 'Liste des artistes ayant une ou plusieurs œuvres présentes sur le site.', description: 'Liste des artistes ayant une ou plusieurs œuvres présentes sur le site.',
creator: '@OrganisationKA', creator: '@OrganisationKA',
images: { images: {
+15 -12
View File
@@ -1,3 +1,4 @@
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'
import ThemeRegistry from './theme-registy' import ThemeRegistry from './theme-registy'
@@ -50,21 +51,23 @@ const jsonLd = {
location: 'Guadeloupe' location: 'Guadeloupe'
} }
export default async function RootLayout({children, Session}) { export default async function RootLayout({children}) {
return ( return (
<html lang='fr' suppressHydrationWarning> <html lang='fr' suppressHydrationWarning>
<body> <body>
<TopLoader color='#ffeb3b' /> <PlausibleProvider src='https://plausible.io/js/pa-3sQidCSfiSOXQUh-4La0T.js'>
<ThemeRegistry> <TopLoader color='#ffeb3b' />
<Navigasyon /> <ThemeRegistry>
{children} <Navigasyon />
</ThemeRegistry> {children}
<section> </ThemeRegistry>
<script <section>
type='application/ld+json' <script
dangerouslySetInnerHTML={{__html: JSON.stringify(jsonLd)}} type='application/ld+json'
/> dangerouslySetInnerHTML={{__html: JSON.stringify(jsonLd)}}
</section> />
</section>
</PlausibleProvider>
</body> </body>
</html> </html>
) )
+8 -4
View File
@@ -1,32 +1,36 @@
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'
import {jwennStats} from '../lib/oki-api' import {jwennStats, jwennDenyeTeks, jwennAnVedette} from '../lib/oki-api'
import Statistik from '../components/akey/statistik' import Statistik from '../components/akey/statistik'
import Akey from '../components/akey' import Akey from '../components/akey'
import AnVedette from '../components/akey/an-vedette'
import okiLogo from '../public/logo-512x512.png' import okiLogo from '../public/logo-512x512.png'
import Footer from '../components/footer' import Footer from '../components/footer'
import Aso from '../components/akey/aso' import Aso from '../components/akey/aso'
async function jwennDone() { async function jwennDone() {
const statistik = await jwennStats() const [statistik, denyeTeks, anVedette] = await Promise.all([jwennStats(), jwennDenyeTeks(), jwennAnVedette()])
if (!statistik) { if (!statistik) {
notFound() notFound()
} }
return statistik return {statistik, dernierTeks: anVedette ?? denyeTeks?.[0]}
} }
export default async function Page() { export default async function Page() {
const statistik = await jwennDone() const {statistik, dernierTeks} = await jwennDone()
return ( return (
<Box sx={{display: 'flex', flexDirection: 'column', minHeight: '100vh'}}> <Box sx={{display: 'flex', flexDirection: 'column', minHeight: '100vh'}}>
<Akey logo={okiLogo} /> <Akey logo={okiLogo} />
<Container sx={{flexGrow: 100}}> <Container sx={{flexGrow: 100}}>
{dernierTeks && <AnVedette teks={dernierTeks} />}
<Statistik statistik={statistik} /> <Statistik statistik={statistik} />
<Aso /> <Aso />
</Container> </Container>
+17 -2
View File
@@ -27,7 +27,7 @@ export async function generateMetadata(props) {
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 awtis = anTeks?.artistes?.length === 1 ? anTeks?.artistes[0].alias : getAlias(anTeks.artistes, anTeks.prioriteArtistes)
const title = `OKI | ${awtis} - ${anTeks.titre} | Paroles et Traductions` 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 url = `${siteUrl}/paroles/${slug}`
@@ -80,8 +80,12 @@ export default async function AnPawolPaj(props) {
'@id': anTeks.musicBrainzUrl || undefined, '@id': anTeks.musicBrainzUrl || undefined,
name: anTeks.titre, name: anTeks.titre,
url: `${siteUrl}/paroles/${slug}`, url: `${siteUrl}/paroles/${slug}`,
inLanguage: anTeks.langueSource ?? 'ka',
image: teksKuvetiFormat?.url ? `${apiUrl}${teksKuvetiFormat?.url}` : undefined, image: teksKuvetiFormat?.url ? `${apiUrl}${teksKuvetiFormat?.url}` : undefined,
thumbnailUrl: couverture?.formats?.thumbnail?.url ? `${apiUrl}${couverture.formats.thumbnail.url}` : undefined, thumbnailUrl: couverture?.formats?.thumbnail?.url ? `${apiUrl}${couverture.formats.thumbnail.url}` : undefined,
license: anTeks.creativeCommons
? `https://creativecommons.org/licenses/${anTeks.creativeCommons.toLowerCase()}/4.0/`
: undefined,
byArtist: anTeks.artistes.map(({photo, musicBrainzUrl, alias, slug}) => { byArtist: anTeks.artistes.map(({photo, musicBrainzUrl, alias, slug}) => {
const kuvetiFormat = formatKuveti(photo) const kuvetiFormat = formatKuveti(photo)
@@ -93,7 +97,18 @@ export default async function AnPawolPaj(props) {
image: kuvetiFormat?.url ? `${apiUrl}${kuvetiFormat?.url}` : undefined image: kuvetiFormat?.url ? `${apiUrl}${kuvetiFormat?.url}` : undefined
} }
}), }),
datePublished: anTeks?.annee datePublished: anTeks?.annee,
potentialAction: anTeks.streamAudio?.length > 0
? anTeks.streamAudio.map(lyen => ({
'@type': 'ListenAction',
target: lyen.url
}))
: undefined,
isBasedOn: anTeks.sourceOriginale ? {
'@type': 'MusicRecording',
name: anTeks.sourceOriginale.titre,
url: `${siteUrl}/paroles/${anTeks.sourceOriginale.slug}`
} : undefined
} }
return ( return (
+36
View File
@@ -2,9 +2,13 @@ import Box from '@mui/material/Box'
import {notFound} from 'next/navigation' import {notFound} from 'next/navigation'
import {jwennDenyeTeks} from '../../lib/oki-api' import {jwennDenyeTeks} from '../../lib/oki-api'
import {formatKuveti} from '../../lib/kuveti'
import DenyeTeks from '../../components/teks/denye-teks' import DenyeTeks from '../../components/teks/denye-teks'
import Footer from '../../components/footer' import Footer from '../../components/footer'
const apiUrl = process.env.NEXT_PUBLIC_API_URL_ROOT || 'http://localhost:1337'
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'
async function jwennDone() { async function jwennDone() {
const denyeTeks = await jwennDenyeTeks() const denyeTeks = await jwennDenyeTeks()
@@ -15,6 +19,38 @@ async function jwennDone() {
return denyeTeks return denyeTeks
} }
export async function generateMetadata() {
const denyeTeks = await jwennDone()
const couverture = formatKuveti(denyeTeks[0]?.couverture)
const imageUrl = couverture?.url ? `${apiUrl}${couverture.url}` : `${siteUrl}/logo-512x512.png`
const imageWidth = couverture?.width || 512
const imageHeight = couverture?.height || 512
const songList = denyeTeks.slice(0, 3).map(t => `${t.artistes[0]?.alias} ${t.titre}`).join(', ')
const description = `Derniers morceaux : ${songList}`
return {
title: 'PAWÒL-NU | Derniers morceaux',
description,
openGraph: {
title: 'PAWÒL-NU | Derniers morceaux',
description,
url: `${siteUrl}/paroles`,
siteName: 'PAWÒL-NU. Paroles et traductions.',
images: [{url: imageUrl, width: imageWidth, height: imageHeight}],
locale: 'fr_FR',
type: 'website',
},
twitter: {
site: '@OrganisationKA',
card: 'summary_large_image',
title: 'PAWÒL-NU | Derniers morceaux',
description,
creator: '@OrganisationKA',
images: {url: imageUrl, alt: 'Couverture du dernier morceau publié'},
},
}
}
export default async function PawolPaj() { export default async function PawolPaj() {
const denyeTeks = await jwennDone() const denyeTeks = await jwennDone()
+73
View File
@@ -0,0 +1,73 @@
'use client'
import PropTypes from 'prop-types'
import Card from '@mui/material/Card'
import CardActionArea from '@mui/material/CardActionArea'
import CardContent from '@mui/material/CardContent'
import Typography from '@mui/material/Typography'
import Box from '@mui/material/Box'
import Chip from '@mui/material/Chip'
import Image from 'next/image'
import Link from 'next/link'
import {getAlias} from '../../lib/utils/format'
import {formatKuveti} from '../../lib/kuveti'
const IMAGE_URL = process.env.NEXT_PUBLIC_API_URL_ROOT || 'http://localhost:1337'
// 1×1 gris neutre — placeholder pendant le chargement
const BLUR_DATA_URL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNsYAAAAAYAAjCB0C8AAAAASUVORK5CYII='
export default function AnVedette({teks}) {
const {titre, artistes, annee, couverture, slug} = teks
const aliases = getAlias(artistes, teks.prioriteArtistes)
const fmt = formatKuveti(couverture, 'medium')
return (
<Box sx={{mb: 4}}>
<Chip
label='NEW RELEASE'
size='small'
color='primary'
sx={{mb: 1.5, fontWeight: 'bold', letterSpacing: 1}}
/>
<Card sx={{borderRadius: 2, overflow: 'hidden'}}>
<CardActionArea component={Link} href={`/paroles/${slug}`}>
<Box sx={{position: 'relative', width: '100%', aspectRatio: {xs: '1 / 1', sm: '16 / 9'}, maxHeight: {sm: 420}}}>
{fmt?.url ? (
<Image
src={`${IMAGE_URL}${fmt.url}`}
alt={titre}
fill
priority
placeholder='blur'
blurDataURL={BLUR_DATA_URL}
sizes='(max-width: 600px) 100vw, 800px'
style={{objectFit: 'cover'}}
/>
) : (
<Box sx={{width: '100%', height: '100%', bgcolor: 'grey.300'}} />
)}
</Box>
<CardContent>
<Typography variant='h5' component='h2' sx={{fontWeight: 'bold', mb: 0.5}}>
{titre}
</Typography>
<Typography variant='body1' color='text.secondary'>
{aliases}
</Typography>
{annee && (
<Typography variant='body2' color='text.secondary'>
{annee}
</Typography>
)}
</CardContent>
</CardActionArea>
</Card>
</Box>
)
}
AnVedette.propTypes = {
teks: PropTypes.object.isRequired
}
+99 -21
View File
@@ -1,6 +1,7 @@
'use client' 'use client'
import {useState} from 'react' import {useState} from 'react'
import Image from 'next/image'
import Link from 'next/link' import Link from 'next/link'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import Accordion from '@mui/material/Accordion' import Accordion from '@mui/material/Accordion'
@@ -15,52 +16,90 @@ import Grid from '@mui/material/Grid'
import Paper from '@mui/material/Paper' import Paper from '@mui/material/Paper'
import Card from '@mui/material/Card' import Card from '@mui/material/Card'
import CardContent from '@mui/material/CardContent' import CardContent from '@mui/material/CardContent'
import Avatar from '@mui/material/Avatar'
import Typography from '@mui/material/Typography' import Typography from '@mui/material/Typography'
import {green} from '@mui/material/colors' import {green} from '@mui/material/colors'
import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
import KeyboardBackspaceIcon from '@mui/icons-material/KeyboardBackspace' import KeyboardBackspaceIcon from '@mui/icons-material/KeyboardBackspace'
import VerifiedIcon from '@mui/icons-material/Verified'
import {formatKuveti} from '../../lib/kuveti' import {formatKuveti} from '../../lib/kuveti'
import {StreamButton} from '../streaming-buttons'
import AwtisBiyografi from './awtis-biyografi' import AwtisBiyografi from './awtis-biyografi'
import MizikLyen from './mizik-lyen' import MizikLyen from './mizik-lyen'
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'
const noImageUrl = 'https://place-hold.it/140x140?text=Indisponible' const BLUR_DATA_URL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNsYAAAAAYAAjCB0C8AAAAASUVORK5CYII='
const sortTeks = paroles => paroles.sort((a, b) => a.titre.localeCompare(b.titre, 'fr', {sensitivity: 'base'})) const sortTeks = paroles => paroles.sort((a, b) => a.titre.localeCompare(b.titre, 'fr', {sensitivity: 'base'}))
export default function AwtisDetay({anAwtis}) { export default function AwtisDetay({anAwtis}) {
const [esByografiOuve, meteEsByografiOuve] = useState(false) const [esByografiOuve, meteEsByografiOuve] = useState(false)
const {alias, biographie, paroles, photo} = anAwtis const {alias, biographie, paroles, photo, isOKIAwtis, titrePhare} = anAwtis
const sortedTeks = sortTeks(paroles) const sortedTeks = sortTeks(paroles)
const gwanBiyo = biographie && biographie.length > 100 const gwanBiyo = biographie && biographie.length > 100
const biyo = gwanBiyo ? `${biographie.slice(0, 100)}...` : biographie const biyo = gwanBiyo ? `${biographie.slice(0, 100)}...` : biographie
const handleClick = () => { const hasStreaming = isOKIAwtis && titrePhare?.streamAudio?.length > 0
meteEsByografiOuve(true) const coverUrl = titrePhare?.couverture
} ? `${IMAGE_URL}${titrePhare.couverture.formats?.small?.url || titrePhare.couverture.formats?.thumbnail?.url || titrePhare.couverture.url}`
: null
const photoUrl = photo?.url
? `${IMAGE_URL}${photo.formats?.small?.url || photo.formats?.thumbnail?.url || photo.url}`
: null
return ( return (
<Container> <Container>
<Box sx={{marginTop: 8, marginBottom: 2}}> <Box sx={{mt: 8, mb: 2}}>
<Typography sx={{textAlign: 'center'}} variant='h6' component='h1'> <Typography sx={{textAlign: 'center'}} variant='h6' component='h1'>
{alias} {alias}
</Typography> </Typography>
</Box> </Box>
<Box sx={{justifyContent: 'center', display: 'flex', marginBottom: 2}}>
<Avatar <Box sx={{display: 'flex', justifyContent: 'center', mb: 2}}>
src={`${photo?.url ? `${IMAGE_URL}${photo?.url}` : noImageUrl}`} <Box sx={{
alt={`Photo ${alias}`} width: 200, height: 200,
sx={{width: 200, height: 200, border: `2px solid ${green[500]}`}} borderRadius: '50%',
/> border: `2px solid ${green[500]}`,
overflow: 'hidden',
position: 'relative',
flexShrink: 0,
}}>
{photoUrl ? (
<Image
src={photoUrl}
alt={`Photo ${alias}`}
width={200}
height={200}
placeholder='blur'
blurDataURL={BLUR_DATA_URL}
style={{objectFit: 'cover', width: '100%', height: '100%'}}
/>
) : (
<Box sx={{width: '100%', height: '100%', bgcolor: 'grey.300'}} />
)}
</Box>
</Box> </Box>
<Grid sx={{alignItems:'center'}} container direction='column' spacing={3}>
{isOKIAwtis && (
<Box sx={{display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 0.75, mb: 2}}>
<Chip
icon={<VerifiedIcon sx={{fontSize: 16}} />}
label='Artiste OKI Exclusif'
size='small'
sx={{bgcolor: '#FFD700', color: '#000', fontWeight: 700, '& .MuiChip-icon': {color: '#000'}}}
/>
<Typography variant='caption' sx={{color: 'text.secondary', textAlign: 'center'}}>
Certains morceaux sont publiés en exclusivité sur PAWÒL-NU, avant toute sortie sur les plateformes de streaming.
</Typography>
</Box>
)}
<Grid sx={{alignItems: 'center'}} container direction='column' spacing={3}>
{biyo && ( {biyo && (
<Grid size={{xs: 12, md: 6}}> <Grid size={{xs: 12, md: 6}}>
<Card sx={{minWidth: 300}}> <Card sx={{minWidth: 300}}>
<CardActionArea onClick={handleClick}> <CardActionArea onClick={() => meteEsByografiOuve(true)}>
<CardContent> <CardContent>
<Typography gutterBottom variant='body1' component='h2'> <Typography gutterBottom variant='body1' component='h2'>
<strong>Biographie</strong> <strong>Biographie</strong>
@@ -73,6 +112,44 @@ export default function AwtisDetay({anAwtis}) {
</Card> </Card>
</Grid> </Grid>
)} )}
{hasStreaming && (
<Grid size={{xs: 12, md: 6}}>
<Card sx={{overflow: 'hidden'}}>
<Grid container>
{coverUrl && (
<Grid size={{xs: 12, sm: 4}}>
<Box sx={{position: 'relative', minHeight: 140, height: '100%'}}>
<Image
src={coverUrl}
alt={titrePhare.titre}
fill
placeholder='blur'
blurDataURL={BLUR_DATA_URL}
sizes='200px'
style={{objectFit: 'cover'}}
/>
</Box>
</Grid>
)}
<Grid size={{xs: 12, sm: coverUrl ? 8 : 12}}>
<CardContent>
<Typography variant='subtitle1' sx={{fontWeight: 700}} gutterBottom>
{titrePhare.titre}
</Typography>
<Typography variant='caption' sx={{color: 'text.secondary', display: 'block', mb: 1.5}}>
Écouter sur
</Typography>
<Box sx={{display: 'flex', flexWrap: 'wrap', gap: 1}}>
{titrePhare.streamAudio.map((lyen, i) => (
<StreamButton key={i} lyen={lyen} />
))}
</Box>
</CardContent>
</Grid>
</Grid>
</Card>
</Grid>
)}
<Grid size={{xs: 12, md: 6}}> <Grid size={{xs: 12, md: 6}}>
<Box marginbottom={3}> <Box marginbottom={3}>
{paroles.length > 1 ? ( {paroles.length > 1 ? (
@@ -82,14 +159,13 @@ export default function AwtisDetay({anAwtis}) {
aria-controls='panel-teks-content' aria-controls='panel-teks-content'
id='panel-teks-header' id='panel-teks-header'
> >
<Typography sx={{marginRight: 2, textAlign:'center' }} variant='body1' component='h2'><strong>Liste des paroles</strong></Typography> <Typography sx={{marginRight: 2, textAlign: 'center'}} variant='body1' component='h2'><strong>Liste des paroles</strong></Typography>
<Chip color='primary' label={paroles.length} size='small' variant='contained' /> <Chip color='primary' label={paroles.length} size='small' variant='contained' />
</AccordionSummary> </AccordionSummary>
<AccordionDetails sx={{paddingInline: 0}}> <AccordionDetails sx={{paddingInline: 0}}>
{sortedTeks.map(anPawol => { {sortedTeks.map(anPawol => {
const {couverture} = anPawol const {couverture} = anPawol
const kuvetiFormat = formatKuveti(couverture) const kuvetiFormat = couverture?.formats?.thumbnail || formatKuveti(couverture)
return ( return (
<Box key={anPawol.id} sx={{paddingBlock: 0.5}}> <Box key={anPawol.id} sx={{paddingBlock: 0.5}}>
<MizikLyen anPawol={anPawol} kuveti={kuvetiFormat} /> <MizikLyen anPawol={anPawol} kuveti={kuvetiFormat} />
@@ -105,7 +181,7 @@ export default function AwtisDetay({anAwtis}) {
<Box> <Box>
<Typography gutterBottom textalign='center' variant='body1' component='h2'><strong>Parole</strong></Typography> <Typography gutterBottom textalign='center' variant='body1' component='h2'><strong>Parole</strong></Typography>
<Paper sx={{height: '100%', paddingBlock: 2}}> <Paper sx={{height: '100%', paddingBlock: 2}}>
<MizikLyen anPawol={paroles[0]} kuveti={formatKuveti(paroles[0].couverture)} /> <MizikLyen anPawol={paroles[0]} kuveti={paroles[0].couverture?.formats?.thumbnail || formatKuveti(paroles[0].couverture)} />
</Paper> </Paper>
</Box> </Box>
) )
@@ -113,13 +189,15 @@ export default function AwtisDetay({anAwtis}) {
</Box> </Box>
</Grid> </Grid>
</Grid> </Grid>
<Box sx={{textAlign: 'center', marginBlock: 3}} >
<Box sx={{textAlign: 'center', marginBlock: 3}}>
<Link passHref href='/awtis'> <Link passHref href='/awtis'>
<Button variant='outlined' startIcon={<KeyboardBackspaceIcon />}> <Button variant='outlined' startIcon={<KeyboardBackspaceIcon />}>
Retour aux artistes Retour aux artistes
</Button> </Button>
</Link> </Link>
</Box> </Box>
{esByografiOuve && ( {esByografiOuve && (
<AwtisBiyografi <AwtisBiyografi
alias={alias} alias={alias}
+20 -4
View File
@@ -10,8 +10,10 @@ import Card from '@mui/material/Card'
import CardMedia from '@mui/material/CardMedia' import CardMedia from '@mui/material/CardMedia'
import CardContent from '@mui/material/CardContent' import CardContent from '@mui/material/CardContent'
import Typography from '@mui/material/Typography' import Typography from '@mui/material/Typography'
import Chip from '@mui/material/Chip'
import {styled} from '@mui/material/styles' import {styled} from '@mui/material/styles'
import VerifiedIcon from '@mui/icons-material/Verified'
import AwtisBiyografi from './awtis-biyografi' import AwtisBiyografi from './awtis-biyografi'
@@ -55,18 +57,32 @@ export default function AwtisKat({artiste}) {
const router = useRouter() const router = useRouter()
const [esByografiOuve, meteEsByografiOuve] = useState(false) const [esByografiOuve, meteEsByografiOuve] = useState(false)
const {alias, biographie, paroles, photo, slug} = artiste const {alias, biographie, paroles, photo, slug, isOKIAwtis} = artiste
return ( return (
<Grid size={{xs: 12, sm: 6, md: 4}}> <Grid size={{xs: 12, sm: 6, md: 4}}>
<Kat> <Kat>
<Card sx={{maxWidth: 340}}> <Card sx={{maxWidth: 340, position: 'relative', ...(isOKIAwtis && {outline: '2px solid #FFD700'})}}>
<CardActionArea onClick={() => router.push(`${SITE_URL}/awtis/${slug}`)}> {isOKIAwtis && (
<Chip
icon={<VerifiedIcon sx={{fontSize: 14}} />}
label='OKI Exclusif'
size='small'
sx={{
position: 'absolute', top: 8, left: 8, zIndex: 1,
bgcolor: '#FFD700', color: '#000',
fontWeight: 700, fontSize: '0.7rem',
'& .MuiChip-icon': {color: '#000'},
}}
/>
)}
<CardActionArea onClick={() => router.push(`/awtis/${slug}`)}>
<CardMedia <CardMedia
className={classes.media} className={classes.media}
component='img' component='img'
alt={alias} alt={alias}
image={`${photo?.url ? `${IMAGE_URL}${photo?.url}` : noImageUrl}`} image={`${photo?.url ? `${IMAGE_URL}${photo?.formats?.thumbnail?.url || photo?.url}` : noImageUrl}`}
loading='lazy'
title={alias} title={alias}
/> />
<CardContent> <CardContent>
+1 -1
View File
@@ -79,7 +79,7 @@ export default function ChecheAwtis() {
<Avatar <Avatar
style={{ marginRight: 8 }} style={{ marginRight: 8 }}
alt={option?.alias} alt={option?.alias}
src={`${IMAGE_URL}${option?.photo?.formats?.thumbnail?.url}`} src={`${IMAGE_URL}${option?.photo?.formats?.thumbnail?.url || option?.photo?.url || ''}`}
/> />
{option?.alias} {option?.alias}
</li> </li>
+1 -1
View File
@@ -34,7 +34,7 @@ export default function MizikLis({niAwtis, paroles, meteEsMobilOuve}) {
itemContent={index => { itemContent={index => {
const anPawol = pawol[index] const anPawol = pawol[index]
const {couverture} = anPawol const {couverture} = anPawol
const kuvetiFormat = formatKuveti(couverture) const kuvetiFormat = couverture?.formats?.thumbnail || formatKuveti(couverture)
return ( return (
<MizikLyen niAwtis={niAwtis} anPawol={anPawol} kuveti={kuvetiFormat} slug={params.slug} meteEsMobilOuve={meteEsMobilOuve} /> <MizikLyen niAwtis={niAwtis} anPawol={anPawol} kuveti={kuvetiFormat} slug={params.slug} meteEsMobilOuve={meteEsMobilOuve} />
+48 -2
View File
@@ -8,11 +8,13 @@ import Box from '@mui/material/Box'
import DialogActions from '@mui/material/DialogActions' import DialogActions from '@mui/material/DialogActions'
import DialogContent from '@mui/material/DialogContent' import DialogContent from '@mui/material/DialogContent'
import DialogTitle from '@mui/material/DialogTitle' import DialogTitle from '@mui/material/DialogTitle'
import Divider from '@mui/material/Divider'
import Link from '@mui/material/Link'
import useMediaQuery from '@mui/material/useMediaQuery' import useMediaQuery from '@mui/material/useMediaQuery'
import {useTheme} from '@mui/material/styles' import {useTheme} from '@mui/material/styles'
import LicensesInfo from './licenses-infos' import LicensesInfo from './licenses-infos'
export default function LicenseModal({license}) { export default function LicenseModal({license, sourceOriginale, remixes}) {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const theme = useTheme() const theme = useTheme()
const fullScreen = useMediaQuery(theme.breakpoints.down('md')) const fullScreen = useMediaQuery(theme.breakpoints.down('md'))
@@ -53,6 +55,48 @@ export default function LicenseModal({license}) {
</Box> </Box>
</DialogTitle> </DialogTitle>
<DialogContent dividers> <DialogContent dividers>
{sourceOriginale && (
<Box sx={{mb: 3}}>
<Typography variant='overline' color='text.secondary' display='block' gutterBottom>
Basé sur
</Typography>
<Link href={`/paroles/${sourceOriginale.slug}`} underline='hover' color='inherit'>
<Typography variant='h6' fontWeight='bold'>{sourceOriginale.titre}</Typography>
</Link>
{sourceOriginale.artistes?.length > 0 && (
<Typography variant='body1' color='text.secondary'>
{sourceOriginale.artistes.map(a => a.alias).join(', ')}
</Typography>
)}
{sourceOriginale.annee && (
<Typography variant='body2' color='text.secondary'>{sourceOriginale.annee}</Typography>
)}
<Divider sx={{mt: 2}} />
</Box>
)}
{remixes && (
<Box sx={{mb: 3}}>
<Typography variant='overline' color='text.secondary' display='block' gutterBottom>
Déclinaisons
</Typography>
{remixes.map(remix => (
<Box key={remix.slug} sx={{mb: 1.5}}>
<Link href={`/paroles/${remix.slug}`} underline='hover' color='inherit'>
<Typography variant='h6' fontWeight='bold'>{remix.titre}</Typography>
</Link>
{remix.artistes?.length > 0 && (
<Typography variant='body1' color='text.secondary'>
{remix.artistes.map(a => a.alias).join(', ')}
</Typography>
)}
{remix.annee && (
<Typography variant='body2' color='text.secondary'>{remix.annee}</Typography>
)}
</Box>
))}
<Divider sx={{mt: 2}} />
</Box>
)}
<LicensesInfo license={license} /> <LicensesInfo license={license} />
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
@@ -66,5 +110,7 @@ export default function LicenseModal({license}) {
} }
LicenseModal.propTypes = { LicenseModal.propTypes = {
license: PropTypes.string.isRequired license: PropTypes.string.isRequired,
sourceOriginale: PropTypes.object,
remixes: PropTypes.array
} }
+1 -1
View File
@@ -95,7 +95,7 @@ export default function Cgu() {
<ArrowRightAltIcon /> <ArrowRightAltIcon />
</ListItemIcon> </ListItemIcon>
<ListItemText> <ListItemText>
<strong>Git</strong> : <Link target='_blank' rel='noreferrer' href='https://codeberg.org/OKI'><strong>codeberg.org/OKI</strong></Link> <strong>Git</strong> : <Link target='_blank' rel='noreferrer' href='https://labola.o-k-i.net/ORGANISATION-KA-INTERNATIONALE'><strong>LABOLA - OKI</strong></Link>
</ListItemText> </ListItemText>
</ListItem> </ListItem>
</List> </List>
+162 -84
View File
@@ -1,9 +1,12 @@
import {useState, useEffect, useRef} from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import List from '@mui/material/List' import List from '@mui/material/List'
import ListSubheader from '@mui/material/ListSubheader' import ListSubheader from '@mui/material/ListSubheader'
import Typography from '@mui/material/Typography' import Typography from '@mui/material/Typography'
import Box from '@mui/material/Box' import Box from '@mui/material/Box'
import {useTheme, useColorScheme, styled} from '@mui/material/styles' import Skeleton from '@mui/material/Skeleton'
import LinearProgress from '@mui/material/LinearProgress'
import {useColorScheme, styled} from '@mui/material/styles'
import Table from '@mui/material/Table' import Table from '@mui/material/Table'
import TableHead from '@mui/material/TableHead' import TableHead from '@mui/material/TableHead'
import TableBody from '@mui/material/TableBody' import TableBody from '@mui/material/TableBody'
@@ -11,12 +14,12 @@ import TableCell, {tableCellClasses} from '@mui/material/TableCell'
import TableRow from '@mui/material/TableRow' import TableRow from '@mui/material/TableRow'
import TableContainer from '@mui/material/TableContainer' import TableContainer from '@mui/material/TableContainer'
import Paper from '@mui/material/Paper' import Paper from '@mui/material/Paper'
import FileSaver from 'file-saver'
import DescriptionIcon from '@mui/icons-material/Description' import DescriptionIcon from '@mui/icons-material/Description'
import LibraryMusicIcon from '@mui/icons-material/LibraryMusic' import LibraryMusicIcon from '@mui/icons-material/LibraryMusic'
import {Link} from '@mui/material' import {Link} from '@mui/material'
const apiUrl = process.env.NEXT_PUBLIC_API_URL_ROOT || 'http://localhost:1337' const apiUrl = process.env.NEXT_PUBLIC_API_URL_ROOT || 'http://localhost:1337'
const audioMetaCache = {}
const StyledTableCell = styled(TableCell)(({theme}) => ({ const StyledTableCell = styled(TableCell)(({theme}) => ({
[`&.${tableCellClasses.head}`]: { [`&.${tableCellClasses.head}`]: {
@@ -47,8 +50,10 @@ function formatSize(size) {
} }
export default function FilesList({files}) { export default function FilesList({files}) {
const theme = useTheme()
const {mode} = useColorScheme() const {mode} = useColorScheme()
const [audioMeta, setAudioMeta] = useState(audioMetaCache)
const [downloading, setDownloading] = useState({})
const controllersRef = useRef({})
const musicFiles = files.filter(file => file.mime.startsWith('audio')) const musicFiles = files.filter(file => file.mime.startsWith('audio'))
const pdfFiles = files.filter(file => file.mime === 'application/pdf') const pdfFiles = files.filter(file => file.mime === 'application/pdf')
@@ -63,84 +68,138 @@ export default function FilesList({files}) {
return extensionOrder[a.ext.toLowerCase()] - extensionOrder[b.ext.toLowerCase()] return extensionOrder[a.ext.toLowerCase()] - extensionOrder[b.ext.toLowerCase()]
}) })
const handleClick = (e, url, fileName) => { useEffect(() => {
e.stopPropagation() const audioFiles = files.filter(f => f.mime.startsWith('audio'))
FileSaver.saveAs(url, fileName) if (audioFiles.length === 0) return
let cancelled = false
async function fetchAllMeta() {
const mm = await import('music-metadata-browser')
const results = {}
await Promise.all(
audioFiles.map(async file => {
if (file.id in audioMetaCache) {
results[file.id] = audioMetaCache[file.id]
return
}
try {
const response = await fetch(`${apiUrl}${file.url}`, {
headers: {Range: 'bytes=0-32767'},
})
const buffer = await response.arrayBuffer()
const meta = await mm.parseBuffer(new Uint8Array(buffer), {mimeType: file.mime, skipCovers: true})
audioMetaCache[file.id] = meta.format
results[file.id] = meta.format
} catch {
audioMetaCache[file.id] = null
results[file.id] = null
}
})
)
if (!cancelled) setAudioMeta(results)
}
fetchAllMeta()
return () => {
cancelled = true
}
}, [files])
const losslessFormats = new Set(['.flac', '.wav', '.aiff', '.alac'])
const qualityBadge = ext => {
const isHaute = losslessFormats.has(ext.toLowerCase())
return (
<Typography variant='caption' sx={{
backgroundColor: isHaute ? '#07332f' : '#393940',
color: isHaute ? '#21feec' : '#fff',
borderRadius: '0.66rem',
border: mode === 'dark' ? `1px solid ${isHaute ? '#21feec' : '#fff'}` : 'none',
padding: '0.15rem 0.5rem',
fontWeight: 'bold',
letterSpacing: '0.05rem',
textTransform: 'uppercase',
}}>{isHaute ? 'Haute' : 'Faible'}</Typography>
)
} }
const getQuality = (extension, caption) => { const renderMeta = file => {
switch (extension) { const format = file.ext.replace('.', '').toUpperCase()
case '.ogg':
case '.aac':
case '.mp3':
return (
<Typography sx={{
backgroundColor: '#393940',
color: '#fff',
borderRadius: '0.66rem',
border: mode === 'dark' ? '1px solid #fff' : 'none',
fontSize: '0.75rem',
letterSpacing: '0.1rem',
padding: '0.2515rem 0.6707rem',
textTransform: 'uppercase',
fontWeight: 'bold'
}}
>Faible</Typography>
)
case '.flac':
if (caption === 'MAX') {
return (
<Typography sx={{
backgroundColor: '#332619',
color: '#ffbe7d',
borderRadius: '0.66rem',
border: mode === 'dark' ? '1px solid #ffbe7d' : 'none',
fontSize: '0.75rem',
letterSpacing: '0.1rem',
padding: '0.2515rem 0.6707rem',
textTransform: 'uppercase',
fontWeight: 'bold',
textAlign: 'center'
}}
>{caption}</Typography>
)
}
if (caption === 'HAUTE') { if (!(file.id in audioMeta)) {
return ( return <Skeleton variant='text' width={80} />
<Typography sx={{
backgroundColor: '#07332f',
color: '#21feec',
borderRadius: '0.66rem',
border: mode === 'dark' ? '1px solid #21feec' : 'none',
fontSize: '0.75rem',
letterSpacing: '0.1rem',
padding: '0.2515rem 0.6707rem',
textTransform: 'uppercase',
fontWeight: 'bold'
}}
>{caption}</Typography>
)
}
return (
<Typography sx={{
backgroundColor: '#07332f',
color: '#21feec',
borderRadius: '0.66rem',
border: mode === 'dark' ? '1px solid #21feec' : 'none',
fontSize: '0.75rem',
letterSpacing: '0.1rem',
padding: '0.2515rem 0.6707rem',
textTransform: 'uppercase',
fontWeight: 'bold'
}}
>Haute</Typography>
)
default:
return <DescriptionIcon sx={{marginRight: 1}} />
} }
const meta = audioMeta[file.id]
const sampleRate = meta?.sampleRate ? `${meta.sampleRate / 1000} kHz` : null
const bitDepth = meta?.bitsPerSample ? `${meta.bitsPerSample} bits` : null
const bitrate = meta?.bitrate ? `${Math.round(meta.bitrate / 1000)} kbps` : null
const details = [sampleRate, bitDepth ?? bitrate].filter(Boolean).join(' · ')
return (
<Box display='flex' flexDirection='column' gap={0.5}>
<Box display='flex' alignItems='center' gap={1}>
{qualityBadge(file.ext)}
<Typography variant='caption' sx={{fontWeight: 'bold', ml: 1}}>{format}</Typography>
</Box>
{details && <Typography variant='caption' sx={{color: 'text.secondary'}}>{details}</Typography>}
</Box>
)
}
useEffect(() => () => {
Object.values(controllersRef.current).forEach(c => c.abort())
}, [])
const handleClick = async (e, url, fileName, fileId) => {
e.stopPropagation()
if (fileId in downloading) return
const controller = new AbortController()
controllersRef.current[fileId] = controller
setDownloading(prev => ({...prev, [fileId]: 0}))
try {
const response = await fetch(url, {signal: controller.signal})
const contentLength = +response.headers.get('content-length')
const reader = response.body.getReader()
const chunks = []
let received = 0
while (true) {
const {done, value} = await reader.read()
if (done) break
chunks.push(value)
received += value.length
if (contentLength) {
setDownloading(prev => ({...prev, [fileId]: Math.round(received / contentLength * 100)}))
}
}
const blob = new Blob(chunks, {type: response.headers.get('content-type')})
const blobUrl = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = blobUrl
a.download = fileName
a.click()
URL.revokeObjectURL(blobUrl)
} catch (error) {
if (error.name !== 'AbortError') throw error
} finally {
delete controllersRef.current[fileId]
setDownloading(prev => {
const next = {...prev}
delete next[fileId]
return next
})
}
}
const handleCancel = (e, fileId) => {
e.stopPropagation()
controllersRef.current[fileId]?.abort()
} }
return ( return (
@@ -151,7 +210,7 @@ export default function FilesList({files}) {
component='nav' component='nav'
aria-labelledby='nested-list-subheader' aria-labelledby='nested-list-subheader'
> >
<ListSubheader disableSticky sx={{backgroundColor: mode === 'light' ? theme.palette.grey[100] : theme.palette.background.default}} color='primary'> <ListSubheader disableSticky sx={{bgcolor: 'background.paper'}}>
<Box paddingBlock={1} display='flex' justifyContent='center'> <Box paddingBlock={1} display='flex' justifyContent='center'>
<LibraryMusicIcon /> <LibraryMusicIcon />
<Typography gutterBottom marginLeft={1} variant='button'>Musiques</Typography> <Typography gutterBottom marginLeft={1} variant='button'>Musiques</Typography>
@@ -159,10 +218,9 @@ export default function FilesList({files}) {
</ListSubheader> </ListSubheader>
<TableContainer component={Paper}> <TableContainer component={Paper}>
<Table size='small' aria-label='Musiques'> <Table size='small' aria-label='Musiques'>
<caption><small><strong>MAX</strong> : <i>Jusqu’à 24-bit, 96 kHz, ≃ 3000 kbps (flac)</i><br /> <strong>HAUTE</strong> : <i>16 bits, ≃ 900 kbps (flac)</i></small><br /> <small><strong>FAIBLE</strong> : <i>320 kbps (ogg / aac / mp3)</i></small></caption>
<TableHead> <TableHead>
<TableRow> <TableRow>
<StyledTableCell align='center'>QUALITÉ</StyledTableCell> <StyledTableCell align='center'>FORMAT</StyledTableCell>
<StyledTableCell /> <StyledTableCell />
</TableRow> </TableRow>
</TableHead> </TableHead>
@@ -170,19 +228,39 @@ export default function FilesList({files}) {
{sortedMusicFiles.map(file => ( {sortedMusicFiles.map(file => (
<StyledTableRow key={file.id}> <StyledTableRow key={file.id}>
<StyledTableCell> <StyledTableCell>
{getQuality(file.ext.toLowerCase(), file?.caption?.toUpperCase())} {renderMeta(file)}
</StyledTableCell> </StyledTableCell>
<StyledTableCell align='left'> <StyledTableCell align='left'>
<Link <Link
href='#' href='#'
underline='hover' underline='hover'
sx={{fontWeight: 'bold'}} sx={{fontWeight: 'bold', pointerEvents: file.id in downloading ? 'none' : 'auto'}}
aria-label='download' aria-label='download'
onClick={e => handleClick(e, `${apiUrl}${file.url}`, file.name)} onClick={e => handleClick(e, `${apiUrl}${file.url}`, file.name, file.id)}
> >
{file.name} {file.name}
</Link> </Link>
<small style={{marginLeft: 3}}>({formatSize(file.size)})</small> <small style={{marginLeft: 3}}>({formatSize(file.size)})</small>
{file.id in downloading && (
<Box sx={{mt: 0.5}}>
<LinearProgress
variant={downloading[file.id] > 0 ? 'determinate' : 'indeterminate'}
value={downloading[file.id]}
/>
<Box display='flex' justifyContent='space-between' alignItems='center'>
<Typography variant='caption' sx={{color: 'text.secondary'}}>
{downloading[file.id]} %
</Typography>
<Typography
variant='caption'
sx={{color: 'error.main', cursor: 'pointer'}}
onClick={e => handleCancel(e, file.id)}
>
Annuler
</Typography>
</Box>
</Box>
)}
</StyledTableCell> </StyledTableCell>
</StyledTableRow> </StyledTableRow>
))} ))}
@@ -198,7 +276,7 @@ export default function FilesList({files}) {
component='nav' component='nav'
aria-labelledby='nested-list-subheader' aria-labelledby='nested-list-subheader'
> >
<ListSubheader disableSticky sx={{marginTop: 2, backgroundColor: mode === 'light' ? theme.palette.grey[100] : theme.palette.background.default}} color='primary'> <ListSubheader disableSticky sx={{marginTop: 2, bgcolor: 'background.paper'}}>
<Box paddingBlock={1} display='flex' justifyContent='center' alignSelf='center'> <Box paddingBlock={1} display='flex' justifyContent='center' alignSelf='center'>
<DescriptionIcon /> <DescriptionIcon />
<Typography gutterBottom marginLeft={1} variant='button'>Paroles</Typography> <Typography gutterBottom marginLeft={1} variant='button'>Paroles</Typography>
+8 -8
View File
@@ -76,7 +76,7 @@ function AjouteTradiksyon({showSwitch, disableSwitch, tradiksyonOtomatik, setTra
aria-haspopup='true' aria-haspopup='true'
onClick={handleClick} onClick={handleClick}
> >
Ajouter une traduction <br /> 🇫🇷 🇬🇧 🇪🇸 🇩🇪 🇮🇹 Ajouter une traduction <br /> 🇺🇸 🇫🇷 🇪🇸 🇩🇪 🇮🇹
</Button> </Button>
<StyledMenu <StyledMenu
keepMounted keepMounted
@@ -88,6 +88,13 @@ function AjouteTradiksyon({showSwitch, disableSwitch, tradiksyonOtomatik, setTra
}} }}
onClose={handleClose} onClose={handleClose}
> >
<StyledMenuItem
id='en'
classes={{
root: classes.root
}}
onClick={handleClose}
>🇺🇸 Anglais (US)</StyledMenuItem>
<StyledMenuItem <StyledMenuItem
id='fr' id='fr'
classes={{ classes={{
@@ -95,13 +102,6 @@ function AjouteTradiksyon({showSwitch, disableSwitch, tradiksyonOtomatik, setTra
}} }}
onClick={handleClose} onClick={handleClose}
>🇫🇷 Français</StyledMenuItem> >🇫🇷 Français</StyledMenuItem>
<StyledMenuItem
id='en'
classes={{
root: classes.root
}}
onClick={handleClose}
>🇬🇧 Anglais</StyledMenuItem>
<StyledMenuItem <StyledMenuItem
id='es' id='es'
classes={{ classes={{
+41
View File
@@ -0,0 +1,41 @@
'use client'
import Button from '@mui/material/Button'
import OpenInNewIcon from '@mui/icons-material/OpenInNew'
import {Spotify, Applemusic, Deezer, Tidal, Youtubemusic, Amazon, Soundcloud} from '@icons-pack/react-simple-icons'
export const PLATFORM_CONFIG = {
Spotify: {label: 'Spotify', bg: '#1DB954', color: '#fff', Icon: Spotify},
Applemusic: {label: 'Apple Music', bg: '#FC3C44', color: '#fff', Icon: Applemusic},
Deezer: {label: 'Deezer', bg: '#EF5466', color: '#fff', Icon: Deezer},
Tidal: {label: 'Tidal', bg: '#000000', color: '#fff', Icon: Tidal},
Qobuz: {label: 'Qobuz', bg: '#00245B', color: '#fff', Icon: null},
Youtubemusic: {label: 'YouTube Music', bg: '#FF0000', color: '#fff', Icon: Youtubemusic},
Amazon: {label: 'Amazon Music', bg: '#00A8E1', color: '#fff', Icon: Amazon},
Soundcloud: {label: 'SoundCloud', bg: '#FF5500', color: '#fff', Icon: Soundcloud},
}
export function StreamButton({lyen}) {
const config = PLATFORM_CONFIG[lyen.plateforme] ?? {label: lyen.plateforme, bg: '#555', color: '#fff', Icon: null}
const PlatformIcon = config.Icon
return (
<Button
href={lyen.url}
target='_blank'
rel='noopener noreferrer'
size='small'
startIcon={PlatformIcon ? <PlatformIcon size={16} color={config.color} /> : null}
endIcon={<OpenInNewIcon sx={{fontSize: 11}} />}
sx={{
bgcolor: config.bg,
color: config.color,
fontWeight: 700,
fontSize: '0.72rem',
textTransform: 'none',
'&:hover': {bgcolor: config.bg, opacity: 0.85},
}}
>
{config.label}
</Button>
)
}
+225
View File
@@ -0,0 +1,225 @@
'use client'
import {useState, useEffect, forwardRef} from 'react'
import PropTypes from 'prop-types'
import Fab from '@mui/material/Fab'
import Dialog from '@mui/material/Dialog'
import Slide from '@mui/material/Slide'
import Box from '@mui/material/Box'
import Skeleton from '@mui/material/Skeleton'
import IconButton from '@mui/material/IconButton'
import Typography from '@mui/material/Typography'
import Tooltip from '@mui/material/Tooltip'
import useMediaQuery from '@mui/material/useMediaQuery'
import {useTheme, keyframes} from '@mui/material/styles'
import MicIcon from '@mui/icons-material/Mic'
import CloseIcon from '@mui/icons-material/Close'
const pulse = keyframes`
0%, 100% { opacity: 0.18; transform: scale(1); }
50% { opacity: 0.55; transform: scale(1.2); }
`
const SlideUp = forwardRef(function SlideUp(props, ref) {
return <Slide direction='up' ref={ref} {...props} />
})
function toEmbedUrl(url) {
try {
const u = new URL(url)
const match = u.pathname.match(/\/(?:videos\/watch|w)\/([^/?#]+)/)
if (match) {
return `${u.origin}/videos/embed/${match[1]}?title=0&warningTitle=0&peertubeLink=0&controlBar=1`
}
return url
} catch {
return url
}
}
export default function KaraokeModal({url, desktopUrl, titre, artistes}) {
const [open, setOpen] = useState(false)
const [loaded, setLoaded] = useState(false)
const theme = useTheme()
const isMobile = useMediaQuery(theme.breakpoints.down('sm'))
const activeUrl = (!isMobile && desktopUrl) ? desktopUrl : url
useEffect(() => {
if (!open) setLoaded(false)
}, [open])
return (
<>
<Tooltip title='Karaoké' placement='left'>
<Fab
color='primary'
size={isMobile ? 'medium' : 'large'}
aria-label='karaoké'
onClick={() => setOpen(true)}
sx={{
position: 'fixed',
bottom: {xs: 24, sm: 32},
right: {xs: 24, sm: 32},
zIndex: 1050,
}}
>
<MicIcon />
</Fab>
</Tooltip>
<Dialog
open={open}
onClose={() => setOpen(false)}
slots={{transition: SlideUp}}
maxWidth={false}
sx={{
'& .MuiDialog-container': {
alignItems: {xs: 'flex-end', sm: 'center'},
}
}}
slotProps={{
backdrop: {sx: {backdropFilter: 'blur(6px)', bgcolor: 'rgba(0,0,0,0.85)'}},
paper: {
sx: {
bgcolor: '#000',
overflow: 'hidden',
...(isMobile ? {
width: '100vw',
height: '100dvh',
maxWidth: 'none',
maxHeight: 'none',
m: 0,
borderRadius: 0,
boxShadow: 'none',
} : {
width: 'calc(80vh * 16 / 9)',
height: '80vh',
maxWidth: 'none',
maxHeight: 'none',
borderRadius: '16px',
boxShadow: '0 24px 64px rgba(0,0,0,0.7)',
})
}
}
}}
>
{/* Header superposé en gradient */}
<Box sx={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
zIndex: 2,
px: 2,
pt: 2,
pb: 4,
background: 'linear-gradient(to bottom, rgba(0,0,0,0.8) 0%, transparent 100%)',
display: 'flex',
alignItems: 'flex-start',
justifyContent: 'space-between',
pointerEvents: 'none',
}}>
<Box sx={{pointerEvents: 'none'}}>
{titre && (
<Typography variant='subtitle1' sx={{color: '#fff', fontWeight: 700, lineHeight: 1.2, textShadow: '0 1px 4px rgba(0,0,0,0.5)'}}>
{titre}
</Typography>
)}
{artistes?.length > 0 && (
<Typography variant='caption' sx={{color: 'rgba(255,255,255,0.65)', textShadow: '0 1px 4px rgba(0,0,0,0.5)'}}>
{artistes.map(a => a.alias).join(', ')}
</Typography>
)}
</Box>
<IconButton
size='small'
onClick={() => setOpen(false)}
aria-label='fermer'
sx={{
pointerEvents: 'auto',
color: '#fff',
bgcolor: 'rgba(255,255,255,0.15)',
backdropFilter: 'blur(8px)',
border: '1px solid rgba(255,255,255,0.12)',
'&:hover': {bgcolor: 'rgba(255,255,255,0.25)'},
}}
>
<CloseIcon fontSize='small' />
</IconButton>
</Box>
{open && (
<Box sx={{position: 'absolute', inset: 0, overflow: 'hidden'}}>
{!loaded && (
<>
<Skeleton
variant='rectangular'
animation='wave'
sx={{position: 'absolute', inset: 0, bgcolor: 'rgba(255,255,255,0.06)', transform: 'none'}}
/>
<Box sx={{
position: 'absolute',
inset: 0,
zIndex: 1,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 1.5,
}}>
<MicIcon sx={{fontSize: 44, color: 'rgba(255,255,255,0.55)', animation: `${pulse} 1.6s ease-in-out infinite`}} />
<Typography
variant='caption'
sx={{color: 'rgba(255,255,255,0.35)', letterSpacing: '0.15em', textTransform: 'uppercase'}}
>
Chargement
</Typography>
</Box>
</>
)}
{isMobile ? (
/* Vidéo verticale : scale le player 16:9 à la hauteur du portrait et croppe les côtés */
<Box sx={{
position: 'absolute',
top: 0,
left: '50%',
transform: 'translateX(-50%)',
height: '100%',
aspectRatio: '16 / 9',
}}>
<iframe
src={toEmbedUrl(activeUrl)}
onLoad={() => setLoaded(true)}
style={{width: '100%', height: '100%', border: 'none', display: 'block', opacity: loaded ? 1 : 0, transition: 'opacity 0.4s ease'}}
allowFullScreen
allow='autoplay; fullscreen'
sandbox='allow-same-origin allow-scripts allow-popups allow-forms'
title='Karaoké'
/>
</Box>
) : (
/* Vidéo 16:9 desktop : player remplit exactement le dialog paysage */
<iframe
src={toEmbedUrl(activeUrl)}
onLoad={() => setLoaded(true)}
style={{width: '100%', height: '100%', border: 'none', display: 'block', opacity: loaded ? 1 : 0, transition: 'opacity 0.4s ease'}}
allowFullScreen
allow='autoplay; fullscreen'
sandbox='allow-same-origin allow-scripts allow-popups allow-forms'
title='Karaoké'
/>
)}
</Box>
)}
</Dialog>
</>
)
}
KaraokeModal.propTypes = {
url: PropTypes.string.isRequired,
desktopUrl: PropTypes.string,
titre: PropTypes.string,
artistes: PropTypes.array,
}
+73 -4
View File
@@ -2,7 +2,7 @@
import {useState, useEffect, useRef} from 'react' import {useState, useEffect, useRef} from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import {styled, useTheme} from '@mui/material/styles' import {styled, useTheme, useColorScheme} from '@mui/material/styles'
import Box from '@mui/material/Box' import Box from '@mui/material/Box'
import Typography from '@mui/material/Typography' import Typography from '@mui/material/Typography'
import Slider from '@mui/material/Slider' import Slider from '@mui/material/Slider'
@@ -20,6 +20,18 @@ import {getAlias} from '../../lib/utils/format'
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'
function parseLrc(text) {
const lines = []
for (const raw of text.split('\n')) {
const match = raw.match(/^\[(\d{2}):(\d{2})[.:]\d+\](.*)$/)
if (match) {
lines.push({time: parseInt(match[1]) * 60 + parseInt(match[2]), text: match[3].trim()})
}
}
return lines.sort((a, b) => a.time - b.time).filter(l => l.text)
}
const Widget = styled('div')(({theme}) => ({ const Widget = styled('div')(({theme}) => ({
padding: 16, padding: 16,
borderRadius: 16, borderRadius: 16,
@@ -59,11 +71,13 @@ export default function Lekte({audio, url, parole}) {
const audioRef = useRef(new Audio(audio)) const audioRef = useRef(new Audio(audio))
const intervalRef = useRef() const intervalRef = useRef()
const isReady = useRef(false) const isReady = useRef(false)
const {duration} = audioRef.current const [duration, setDuration] = useState(0)
const theme = useTheme() const theme = useTheme()
const {mode} = useColorScheme()
const [position, setPosition] = useState(0) const [position, setPosition] = useState(0)
const [volume, setVolume] = useState(100) const [volume, setVolume] = useState(100)
const [isPlaying, setIsPlaying] = useState(false) const [isPlaying, setIsPlaying] = useState(false)
const [lrcLines, setLrcLines] = useState(null)
const alias = getAlias(parole.artistes, parole.prioriteArtistes) const alias = getAlias(parole.artistes, parole.prioriteArtistes)
function formatDuration(value) { function formatDuration(value) {
@@ -76,8 +90,8 @@ export default function Lekte({audio, url, parole}) {
return `${minute}:${secondLeft <= 9 ? `0${secondLeft}` : secondLeft}` return `${minute}:${secondLeft <= 9 ? `0${secondLeft}` : secondLeft}`
} }
const mainIconColor = theme.palette.mode === 'dark' ? '#fff' : '#000' const mainIconColor = mode === 'dark' ? '#fff' : '#000'
const lightIconColor = theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.4)' : grey[900] const lightIconColor = mode === 'dark' ? 'rgba(255,255,255,0.4)' : grey[900]
const startTimer = () => { const startTimer = () => {
clearInterval(intervalRef.current) clearInterval(intervalRef.current)
@@ -125,8 +139,38 @@ export default function Lekte({audio, url, parole}) {
setIsPlaying(false) setIsPlaying(false)
setVolume(100) setVolume(100)
setPosition(0) setPosition(0)
setDuration(0)
}, [audio]) }, [audio])
useEffect(() => {
const el = audioRef.current
const onLoaded = () => setDuration(Math.round(el.duration))
const onEnded = () => {
clearInterval(intervalRef.current)
setIsPlaying(false)
setPosition(0)
el.currentTime = 0
}
el.addEventListener('loadedmetadata', onLoaded)
el.addEventListener('ended', onEnded)
return () => {
el.removeEventListener('loadedmetadata', onLoaded)
el.removeEventListener('ended', onEnded)
}
}, [audio])
useEffect(() => {
if (!parole?.pawol?.url) return
fetch(new URL(parole.pawol.url, IMAGE_URL).toString())
.then(r => r.text())
.then(text => setLrcLines(parseLrc(text)))
.catch(() => setLrcLines([]))
}, [parole?.pawol?.url])
const activeIndex = lrcLines
? lrcLines.reduce((last, line, i) => line.time <= position ? i : last, -1)
: -1
const handleChangePosition = value => { const handleChangePosition = value => {
setPosition(value) setPosition(value)
audioRef.current.currentTime = value audioRef.current.currentTime = value
@@ -273,6 +317,31 @@ export default function Lekte({audio, url, parole}) {
/> />
<VolumeUpRounded htmlColor={lightIconColor} /> <VolumeUpRounded htmlColor={lightIconColor} />
</Stack> </Stack>
{lrcLines && lrcLines.length > 0 && (
<Box sx={{mt: 1.5, textAlign: 'center', minHeight: 64}}>
{[-1, 0, 1].map(offset => {
const idx = activeIndex + offset
const line = lrcLines[idx]
if (!line) return <Box key={offset} sx={{height: offset === 0 ? 28 : 20}} />
return (
<Typography
key={offset}
variant={offset === 0 ? 'body2' : 'caption'}
sx={{
display: 'block',
fontWeight: offset === 0 ? 'bold' : 'normal',
color: offset === 0 ? 'text.primary' : 'text.secondary',
opacity: offset === 0 ? 1 : 0.45,
transition: 'all 0.3s ease',
lineHeight: offset === 0 ? 1.6 : 1.4,
}}
>
{line.text}
</Typography>
)
})}
</Box>
)}
</Widget> </Widget>
</Box> </Box>
) )
+4 -3
View File
@@ -18,6 +18,7 @@ import VweKouteAchte from './vwe-koute-achte'
import DrawerBar from './drawer-bar' import DrawerBar from './drawer-bar'
import Pataje from './pataje' import Pataje from './pataje'
import KaraokeModal from './karaoke-modal'
const drawerWidth = 240 const drawerWidth = 240
@@ -118,9 +119,6 @@ export default function TeksDrawer({paroles}) {
<Drawer <Drawer
variant='temporary' variant='temporary'
open={mobileOpen} open={mobileOpen}
ModalProps={{
keepMounted: true
}}
sx={{ sx={{
display: {xs: 'block', sm: 'none'}, display: {xs: 'block', sm: 'none'},
'& .MuiDrawer-paper': {boxSizing: 'border-box', width: drawerWidth} '& .MuiDrawer-paper': {boxSizing: 'border-box', width: drawerWidth}
@@ -140,6 +138,9 @@ export default function TeksDrawer({paroles}) {
<DrawerBar meteEsMobilOuve={setMobileOpen} paroles={paroles} /> <DrawerBar meteEsMobilOuve={setMobileOpen} paroles={paroles} />
</Drawer> </Drawer>
</Box> </Box>
{parole?.karaokeUrl && (
<KaraokeModal url={parole.karaokeUrl} desktopUrl={parole.karaokeDesktopUrl} titre={parole.titre} artistes={parole.artistes} />
)}
{success && ( {success && (
<Snackbar open={open} autoHideDuration={3000} onClose={handleClose}> <Snackbar open={open} autoHideDuration={3000} onClose={handleClose}>
<Alert severity='success' onClose={handleClose}> <Alert severity='success' onClose={handleClose}>
+18 -16
View File
@@ -9,7 +9,6 @@ import Card from '@mui/material/Card'
import CardActionArea from '@mui/material/CardActionArea' import CardActionArea from '@mui/material/CardActionArea'
import CardContent from '@mui/material/CardContent' import CardContent from '@mui/material/CardContent'
import CardMedia from '@mui/material/CardMedia'
import Typography from '@mui/material/Typography' 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'
@@ -23,26 +22,21 @@ const IMAGE_URL = process.env.NEXT_PUBLIC_API_URL_ROOT || 'http://localhost:1337
const classes = { const classes = {
root: `${PREFIX}-root`, root: `${PREFIX}-root`,
media: `${PREFIX}-media`
} }
const StyledGrid = styled(Grid)({ const StyledGrid = styled(Grid)({
[`& .${classes.root}`]: { [`& .${classes.root}`]: {
maxWidth: 345 maxWidth: 345
}, },
[`& .${classes.media}`]: {
height: 240,
objectFit: 'contain'
}
}) })
const noImageUrl = 'https://place-hold.it/140x140?text=Indisponible' const BLUR_DATA_URL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNsYAAAAAYAAjCB0C8AAAAASUVORK5CYII='
export default function TeksKat({parole}) { export default function TeksKat({parole}) {
const router = useRouter() const router = useRouter()
const {titre, artistes, annee, couverture, publishedAt, slug} = parole const {titre, artistes, annee, couverture, createdAt, slug} = parole
const datPiblikasyon = format(new Date(publishedAt), 'P', {locale: fr}) const datPiblikasyon = format(new Date(createdAt), 'P', {locale: fr})
const aliases = getAlias(artistes, parole.prioriteArtistes) const aliases = getAlias(artistes, parole.prioriteArtistes)
const handleClick = slug => { const handleClick = slug => {
@@ -53,13 +47,21 @@ export default function TeksKat({parole}) {
<StyledGrid size={{xs: 12, sm: 6, md: 4}}> <StyledGrid size={{xs: 12, sm: 6, md: 4}}>
<Card className={classes.root}> <Card className={classes.root}>
<CardActionArea onClick={() => handleClick(slug)}> <CardActionArea onClick={() => handleClick(slug)}>
<CardMedia <Box sx={{position: 'relative', height: 240}}>
className={classes.media} {couverture?.url ? (
component='img' <Image
alt={titre} src={`${IMAGE_URL}${couverture.formats?.thumbnail?.url || couverture.url}`}
image={couverture?.url ? `${IMAGE_URL}${couverture.url}` : noImageUrl} alt={titre}
title={titre} fill
/> placeholder='blur'
blurDataURL={BLUR_DATA_URL}
sizes='(max-width: 600px) 100vw, (max-width: 900px) 50vw, 33vw'
style={{objectFit: 'contain'}}
/>
) : (
<Box sx={{width: '100%', height: '100%', bgcolor: 'grey.300'}} />
)}
</Box>
<CardContent> <CardContent>
<Box sx={{display: 'flex', alignItems: 'center'}}> <Box sx={{display: 'flex', alignItems: 'center'}}>
<Typography display='inline' style={{marginRight: 5}} variant='h6' component='h2'> <Typography display='inline' style={{marginRight: 5}} variant='h6' component='h2'>
+47 -34
View File
@@ -13,14 +13,20 @@ 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 Image from 'next/image'
import {formatJsonString, getAlias} from '../../lib/utils/format' import {formatJsonString, getAlias} from '../../lib/utils/format'
import {formatKuveti} from '../../lib/kuveti'
import LicenseModal from '../cc/license-modal' import LicenseModal from '../cc/license-modal'
import FilesDialog from '../files/files-dialog' import FilesDialog from '../files/files-dialog'
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'
const IMAGE_URL = process.env.NEXT_PUBLIC_API_URL_ROOT || 'http://localhost:1337'
const PREFIX = 'teks' const PREFIX = 'teks'
const classes = { const classes = {
@@ -72,30 +78,32 @@ const Root = styled('div')((
}, },
})) }))
const LANG_NAMES = {fr: 'Français', en: 'English', es: 'Español', de: 'Deutsch', it: 'Italiano'}
const langToArray = parole => { const langToArray = parole => {
const langArray = [] const langArray = []
if (parole && parole.traductions) { if (parole && parole.traductions) {
const {francais, anglais, espagnol, allemand, italien} = parole.traductions const {francais, anglais, espagnol, allemand, italien} = parole.traductions
if (francais) { if (anglais) {
langArray.push({title: 'Traduction', flag: 'fr', lang: francais}) langArray.push({title: 'English', lang: anglais})
} }
if (anglais) { if (francais) {
langArray.push({title: 'Translation', flag: 'en', lang: anglais}) langArray.push({title: 'Français', lang: francais})
} }
if (espagnol) { if (espagnol) {
langArray.push({title: 'Traducción', flag: 'es', lang: espagnol}) langArray.push({title: 'Español', lang: espagnol})
} }
if (allemand) { if (allemand) {
langArray.push({title: 'Übersetzung', flag: 'de', lang: allemand}) langArray.push({title: 'Deutsch', lang: allemand})
} }
if (italien) { if (italien) {
langArray.push({title: 'Traduzione', flag: 'it', lang: italien}) langArray.push({title: 'Italiano', lang: italien})
} }
} }
@@ -126,6 +134,7 @@ export default function Teks({parole}) {
const isMobile = useMediaQuery('(max-width:600px)') const isMobile = useMediaQuery('(max-width:600px)')
const langArray = langToArray(parole) const langArray = langToArray(parole)
const enhancedAliases = getAlias(parole.artistes, parole.prioriteArtistes, true) const enhancedAliases = getAlias(parole.artistes, parole.prioriteArtistes, true)
const coverFmt = formatKuveti(parole.couverture)
useEffect(() => { useEffect(() => {
const isBrowser = () => typeof window !== 'undefined' const isBrowser = () => typeof window !== 'undefined'
@@ -169,6 +178,17 @@ export default function Teks({parole}) {
</Box> </Box>
</Typography> </Typography>
{coverFmt?.url && (
<Box sx={{display: 'flex', justifyContent: 'center', mb: 2}}>
<Image
src={new URL(coverFmt.url, IMAGE_URL).toString()}
alt={parole.titre}
width={coverFmt.width || 300}
height={coverFmt.height || 300}
style={{maxWidth: '100%', maxHeight: 320, width: 'auto', height: 'auto', borderRadius: 8}}
/>
</Box>
)}
{parole?.user && ( {parole?.user && (
<Typography style={{marginBottom: '1.5em'}} display='block' variant='caption'> <Typography style={{marginBottom: '1.5em'}} display='block' variant='caption'>
<i>parole soumise par {parole.user.username}</i> <i>parole soumise par {parole.user.username}</i>
@@ -181,7 +201,7 @@ export default function Teks({parole}) {
)} )}
{parole.creativeCommons && ( {parole.creativeCommons && (
<Box sx={{maxWidth: 220, margin: '0 auto', textAlign: 'center'}}> <Box sx={{maxWidth: 220, margin: '0 auto', textAlign: 'center'}}>
<LicenseModal license={parole.creativeCommons.toLowerCase()} /> <LicenseModal license={parole.creativeCommons.toLowerCase()} sourceOriginale={parole.sourceOriginale ?? null} remixes={parole.remixes?.length ? parole.remixes : null} />
</Box> </Box>
)} )}
{parole?.files && ( {parole?.files && (
@@ -198,47 +218,40 @@ export default function Teks({parole}) {
{parole?.difference?.length > 0 && ( {parole?.difference?.length > 0 && (
<DiferansDialog difference={parole.difference} /> <DiferansDialog difference={parole.difference} />
)} )}
{parole.streamAudio?.length > 0 && (
<Box sx={{mt: 2, mb: 1}}>
<Typography variant='overline' sx={{color: 'text.secondary', display: 'block', mb: 1}}>
Écouter sur
</Typography>
<Box sx={{display: 'flex', flexWrap: 'wrap', justifyContent: 'center', gap: 1}}>
{parole.streamAudio.map((lyen, i) => (
<StreamButton key={i} lyen={lyen} />
))}
</Box>
</Box>
)}
</Box> </Box>
<Grid container justifycontent='center' spacing={1}> <Grid container justifycontent='center' spacing={1}>
<Grid size={{xs: 12, md: langArray.length > 0 ? 6 : null}}> <Grid size={{xs: 12, md: langArray.length > 0 ? 6 : null}}>
<Box className={classes.gridText}> <Box className={classes.gridText}>
<Typography align='center' sx={{marginBottom: '0.5em'}} variant='h4'> <Typography align='center' sx={{marginBottom: '0.5em'}} variant='h4'>
Transcription Transcription
{parole.langueSource && parole.langueSource !== 'ka' && (
<Typography component='span' variant='body2' color='text.secondary' sx={{ml: 1}}>
({LANG_NAMES[parole.langueSource]})
</Typography>
)}
</Typography> </Typography>
<Typography paragraph='true' align={alignTeks(langArray, isMobile)} component='span'> <Typography paragraph='true' align={alignTeks(langArray, isMobile)} component='span'>
{formatJsonString(parole.transcription)} {formatJsonString(parole.transcription)}
</Typography> </Typography>
</Box> </Box>
</Grid> </Grid>
{langArray.map(({title, flag, lang}) => ( {langArray.map(({title, lang}) => (
<Grid key={title} size={{xs: 12, md: 6}}> <Grid key={title} size={{xs: 12, md: 6}}>
<Box className={classes.gridText}> <Box className={classes.gridText}>
<Typography align='center' sx={{marginBottom: '0.5em'}} variant='h4'> <Typography align='center' sx={{marginBottom: '0.5em'}} variant='h4'>
{flag === 'fr' && ( {title}
<span>
🇫🇷
</span>
)}
{flag === 'en' && (
<span>
🇬🇧
</span>
)}
{flag === 'es' && (
<span>
🇪🇸
</span>
)}
{flag === 'de' && (
<span>
🇩🇪
</span>
)}
{flag === 'it' && (
<span>
🇮🇹
</span>
)} {title}
</Typography> </Typography>
<Typography paragraph='true' align='justify' component='span'> <Typography paragraph='true' align='justify' component='span'>
{formatJsonString(lang)} {formatJsonString(lang)}
+9
View File
@@ -0,0 +1,9 @@
import {SvgIcon} from '@mui/material'
export default function GiteaIcon(props) {
return (
<SvgIcon {...props}>
<path d='M4.186 5.421C2.341 5.417-.13 6.59.006 9.531c.213 4.594 4.92 5.02 6.801 5.057.206.862 2.42 3.834 4.059 3.99h7.18c4.306-.286 7.53-13.022 5.14-13.07-3.953.186-6.296.28-8.305.296v3.975l-.626-.277-.004-3.696c-2.306-.001-4.336-.108-8.189-.298-.482-.003-1.154-.085-1.876-.087zm.261 1.625h.22c.262 2.355.688 3.732 1.55 5.836-2.2-.26-4.072-.899-4.416-3.285-.178-1.235.422-2.524 2.646-2.552zm8.557 2.315c.15.002.303.03.447.096l.749.323-.537.979a.672.597 0 0 0-.241.038.672.597 0 0 0-.405.764.672.597 0 0 0 .112.174l-.926 1.686a.672.597 0 0 0-.222.038.672.597 0 0 0-.405.764.672.597 0 0 0 .86.36.672.597 0 0 0 .404-.765.672.597 0 0 0-.158-.22l.902-1.642a.672.597 0 0 0 .293-.03.672.597 0 0 0 .213-.112c.348.146.633.265.838.366.308.152.417.253.45.365.033.11-.003.322-.177.694-.13.277-.345.67-.599 1.133a.672.597 0 0 0-.251.038.672.597 0 0 0-.405.764.672.597 0 0 0 .86.36.672.597 0 0 0 .404-.764.672.597 0 0 0-.137-.202c.251-.458.467-.852.606-1.148.188-.402.286-.701.2-.99-.086-.289-.35-.477-.7-.65-.23-.113-.517-.233-.86-.377a.672.597 0 0 0-.038-.239.672.597 0 0 0-.145-.209l.528-.963 2.924 1.263c.528.229.746.79.49 1.26l-2.01 3.68c-.257.469-.888.663-1.416.435l-4.137-1.788c-.528-.228-.747-.79-.49-1.26l2.01-3.679c.176-.323.53-.515.905-.53h.064z' />
</SvgIcon>
)
}
+10 -15
View File
@@ -1,19 +1,14 @@
export const formatKuveti = kuveti => { const SIZE_ORDER = {
if (!kuveti) { large: ['large', 'medium', 'small'],
return null medium: ['medium', 'small', 'large'],
} small: ['small', 'medium', 'large'],
}
if (kuveti && kuveti.formats && kuveti.formats.large) { export const formatKuveti = (kuveti, preferred = 'large') => {
return kuveti.formats.large if (!kuveti) return null
const order = SIZE_ORDER[preferred] ?? SIZE_ORDER.large
for (const size of order) {
if (kuveti.formats?.[size]) return kuveti.formats[size]
} }
if (kuveti && kuveti.formats && kuveti.formats.medium) {
return kuveti.formats.medium
}
if (kuveti && kuveti.formats && kuveti.formats.small) {
return kuveti.formats.small
}
return kuveti return kuveti
} }
+40 -2
View File
@@ -56,6 +56,25 @@ export async function jwennTeksEpiSlug(slug) {
}, },
traductions: { traductions: {
populate: '*' populate: '*'
},
pawol: {
populate: '*'
},
sourceOriginale: {
fields: ['titre', 'slug', 'annee'],
populate: {
artistes: {
fields: ['alias', 'slug']
}
}
},
remixes: {
fields: ['titre', 'slug', 'annee'],
populate: {
artistes: {
fields: ['alias', 'slug']
}
}
} }
}, },
filters: { filters: {
@@ -73,7 +92,7 @@ export async function jwennTeksEpiSlug(slug) {
export async function jwennAwtisEpiSlug(slug) { export async function jwennAwtisEpiSlug(slug) {
const query = qs.stringify({ const query = qs.stringify({
populate: ['paroles', 'photo', 'paroles.couverture'], populate: ['paroles', 'photo', 'paroles.couverture', 'titrePhare', 'titrePhare.streamAudio', 'titrePhare.streamVideo', 'titrePhare.couverture'],
filters: { filters: {
slug: { slug: {
$eq: slug $eq: slug
@@ -92,7 +111,7 @@ export async function jwennAwtisPajinasyon(paj) {
const start = AWTIS_POU_CHAK_PAJ * (paj - 1) const start = AWTIS_POU_CHAK_PAJ * (paj - 1)
const query = qs.stringify({ const query = qs.stringify({
populate: ['paroles', 'photo'], populate: ['paroles', 'photo'],
sort: ['createdAt:desc'], sort: ['isOKIAwtis:desc', 'createdAt:desc'],
pagination: { pagination: {
start, start,
limit: AWTIS_POU_CHAK_PAJ limit: AWTIS_POU_CHAK_PAJ
@@ -210,6 +229,25 @@ export async function jwennDenyeTeks() {
return data return data
} }
export async function jwennAnVedette() {
const query = qs.stringify({
populate: ['artistes', 'couverture'],
filters: {
isNewRelease: { $eq: true }
},
pagination: { limit: 1 }
}, {
encodeValuesOnly: true
})
const {data} = await request(`/paroles?${query}`, {
next: {revalidate: 0},
headers: {Authorization: `Bearer ${readToken}`}
})
return data?.[0] ?? null
}
export async function jwennAnTeks(teksId, token) { export async function jwennAnTeks(teksId, token) {
const headers = { const headers = {
'content-type': 'application/json', 'content-type': 'application/json',
+7 -17
View File
@@ -4,10 +4,10 @@ import TelegramIcon from '@mui/icons-material/Telegram'
import PeertubeIcon from './icons/peertube' import PeertubeIcon from './icons/peertube'
import XmppIcon from './icons/xmpp' import XmppIcon from './icons/xmpp'
import XIcon from './icons/x' import XIcon from './icons/x'
import CodebergIcon from './icons/codeberg' import GiteaIcon from './icons/gitea'
import BlueskyIcon from './icons/bluesky' import BlueskyIcon from './icons/bluesky'
const codebergURL = process.env.NEXT_PUBLIC_CODEBERG || 'https://codeberg.org/OKI' const gitURL = process.env.NEXT_PUBLIC_GIT || 'https://labola.o-k-i.net/ORGANISATION-KA-INTERNATIONALE/pawol.nu'
const blueskyUrl = process.env.NEXT_PUBLIC_BLUESKY_URL || 'https://bsky.app/profile/organisationka.bsky.social' const blueskyUrl = process.env.NEXT_PUBLIC_BLUESKY_URL || 'https://bsky.app/profile/organisationka.bsky.social'
const xmppContact = process.env.NEXT_PUBLIC_XMPP || 'oki@xmpp.cz' const xmppContact = process.env.NEXT_PUBLIC_XMPP || 'oki@xmpp.cz'
const gadeUsername = process.env.NEXT_PUBLIC_GADE_USERNAME || 'oki' const gadeUsername = process.env.NEXT_PUBLIC_GADE_USERNAME || 'oki'
@@ -17,25 +17,15 @@ const telegramGroup = process.env.NEXT_PUBLIC_TELEGRAM_GROUP || 'OrganisationKA'
export const rezoLis = [ export const rezoLis = [
{ {
tit: 'Codeberg', tit: 'Git',
lyen: codebergURL, lyen: gitURL,
icon: <CodebergIcon /> icon: <GiteaIcon />
}, },
{ {
tit: 'Bluesky', tit: 'Bluesky',
lyen: blueskyUrl, lyen: blueskyUrl,
icon: <BlueskyIcon /> icon: <BlueskyIcon />
}, },
{
tit: 'XMPP',
lyen: `xmpp:${xmppContact}`,
icon: <XmppIcon />
},
{
tit: 'Gadé',
lyen: `https://gade.o-k-i.net/a/${gadeUsername}/video-channels`,
icon: <PeertubeIcon />
},
{ {
tit: 'Telegram', tit: 'Telegram',
lyen: `https://t.me/${telegramGroup}`, lyen: `https://t.me/${telegramGroup}`,
@@ -47,8 +37,8 @@ export const rezoLis = [
icon: <YouTubeIcon /> icon: <YouTubeIcon />
}, },
{ {
tit: 'Twitter', tit: 'X',
lyen: `https://twitter.com/${tiwtterUsername}`, lyen: `https://x.com/${tiwtterUsername}`,
icon: <XIcon /> icon: <XIcon />
} }
] ]
+6 -4
View File
@@ -5,13 +5,13 @@
"private": false, "private": false,
"license": "AGPL-3.0", "license": "AGPL-3.0",
"author": { "author": {
"name": "Cédric Famibelle-Pronzola", "name": "ORGANSATION KA INTERNATIONALE",
"email": "contact@cedric-pronzola.dev", "email": "kontak@o-k-i.net",
"url": "https://cedric-pronzola.dev" "url": "https://o-k-i.net"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://codeberg.org/OKI/pawol.nu.git" "url": "git+https://labola.o-k-i.net/ORGANISATION-KA-INTERNATIONALE/api.pawol.nu"
}, },
"scripts": { "scripts": {
"lint": "xo", "lint": "xo",
@@ -40,8 +40,10 @@
"express": "^4.17.1", "express": "^4.17.1",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"music-metadata-browser": "^2.5.11",
"next": "16.2.4", "next": "16.2.4",
"next-auth": "^5.0.0-beta.31", "next-auth": "^5.0.0-beta.31",
"next-plausible": "^4.0.0",
"next-pwa": "5.6.0", "next-pwa": "5.6.0",
"nextjs-toploader": "^3.9.17", "nextjs-toploader": "^3.9.17",
"nodemailer": "^6.7.2", "nodemailer": "^6.7.2",
+124 -2
View File
@@ -3212,6 +3212,11 @@
dependencies: dependencies:
tslib "^2.8.0" tslib "^2.8.0"
"@tokenizer/token@^0.3.0":
version "0.3.0"
resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.3.0.tgz#fe98a93fe789247e998c75e74e9c7c63217aa276"
integrity sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==
"@trysound/sax@0.2.0": "@trysound/sax@0.2.0":
version "0.2.0" version "0.2.0"
resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad"
@@ -3618,6 +3623,13 @@
stylis "^4.3.0" stylis "^4.3.0"
ts-invariant "^0.10.3" ts-invariant "^0.10.3"
abort-controller@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==
dependencies:
event-target-shim "^5.0.0"
accepts@~1.3.5, accepts@~1.3.7: accepts@~1.3.5, accepts@~1.3.7:
version "1.3.7" version "1.3.7"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
@@ -4184,6 +4196,14 @@ buffer@^5.5.0:
base64-js "^1.3.1" base64-js "^1.3.1"
ieee754 "^1.1.13" ieee754 "^1.1.13"
buffer@^6.0.3:
version "6.0.3"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6"
integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==
dependencies:
base64-js "^1.3.1"
ieee754 "^1.2.1"
builtin-modules@^3.0.0, builtin-modules@^3.1.0: builtin-modules@^3.0.0, builtin-modules@^3.1.0:
version "3.2.0" version "3.2.0"
resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.2.0.tgz#45d5db99e7ee5e6bc4f362e008bf917ab5049887" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.2.0.tgz#45d5db99e7ee5e6bc4f362e008bf917ab5049887"
@@ -4449,6 +4469,11 @@ content-disposition@0.5.3:
dependencies: dependencies:
safe-buffer "5.1.2" safe-buffer "5.1.2"
content-type@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918"
integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
content-type@~1.0.4: content-type@~1.0.4:
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
@@ -5708,6 +5733,16 @@ etag@~1.8.1:
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
event-target-shim@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
events@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
execa@^5.1.1: execa@^5.1.1:
version "5.1.1" version "5.1.1"
resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"
@@ -5841,6 +5876,15 @@ file-saver@^2.0.5:
resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38" resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38"
integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA== integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==
file-type@^16.5.4:
version "16.5.4"
resolved "https://registry.yarnpkg.com/file-type/-/file-type-16.5.4.tgz#474fb4f704bee427681f98dd390058a172a6c2fd"
integrity sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==
dependencies:
readable-web-to-node-stream "^3.0.0"
strtok3 "^6.2.4"
token-types "^4.1.1"
filelist@^1.0.1: filelist@^1.0.1:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.2.tgz#80202f21462d4d1c2e214119b1807c1bc0380e5b" resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.2.tgz#80202f21462d4d1c2e214119b1807c1bc0380e5b"
@@ -6427,7 +6471,7 @@ idb@^7.0.1:
resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.0.tgz#2cc886be57738419e57f9aab58f647e5e2160270" resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.0.tgz#2cc886be57738419e57f9aab58f647e5e2160270"
integrity sha512-Wsk07aAxDsntgYJY4h0knZJuTxM73eQ4reRAO+Z1liOh8eMCJ/MoDS8fCui1vGT9mnjtl1sOu3I2i/W1swPYZg== integrity sha512-Wsk07aAxDsntgYJY4h0knZJuTxM73eQ4reRAO+Z1liOh8eMCJ/MoDS8fCui1vGT9mnjtl1sOu3I2i/W1swPYZg==
ieee754@^1.1.13: ieee754@^1.1.13, ieee754@^1.2.1:
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
@@ -7479,6 +7523,11 @@ media-typer@0.3.0:
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
media-typer@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-1.1.0.tgz#6ab74b8f2d3320f2064b2a87a38e7931ff3a5561"
integrity sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==
memory-fs@^0.2.0: memory-fs@^0.2.0:
version "0.2.0" version "0.2.0"
resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.2.0.tgz#f2bb25368bc121e391c2520de92969caee0a0290" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.2.0.tgz#f2bb25368bc121e391c2520de92969caee0a0290"
@@ -7657,6 +7706,30 @@ multipipe@^1.0.2:
duplexer2 "^0.1.2" duplexer2 "^0.1.2"
object-assign "^4.1.0" object-assign "^4.1.0"
music-metadata-browser@^2.5.11:
version "2.5.11"
resolved "https://registry.yarnpkg.com/music-metadata-browser/-/music-metadata-browser-2.5.11.tgz#dd28bc6506075ac46ce33f72e6828742b4b6cb9e"
integrity sha512-Khq5nYapffIet0PUVb5J69pZPgqgn+/yoEr0jkO/OjH5xwfdz6rdwj0zsWPaqo3ylv+OthXoGjT6EegVHbMkJQ==
dependencies:
buffer "^6.0.3"
debug "^4.3.4"
music-metadata "^7.13.3"
readable-stream "^4.3.0"
readable-web-to-node-stream "^3.0.2"
music-metadata@^7.13.3:
version "7.14.0"
resolved "https://registry.yarnpkg.com/music-metadata/-/music-metadata-7.14.0.tgz#74e3e5fc8e09b86d1a3e791fb5ce9ccdc4347ad9"
integrity sha512-xrm3w7SV0Wk+OythZcSbaI8mcr/KHd0knJieu8bVpaPfMv/Agz5EooCAPz3OR5hbYMiUG6dgAPKZKnMzV+3amA==
dependencies:
"@tokenizer/token" "^0.3.0"
content-type "^1.0.5"
debug "^4.3.4"
file-type "^16.5.4"
media-typer "^1.1.0"
strtok3 "^6.3.0"
token-types "^4.2.1"
nanoid@^3.3.6: nanoid@^3.3.6:
version "3.3.7" version "3.3.7"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
@@ -7689,6 +7762,11 @@ next-auth@^5.0.0-beta.31:
dependencies: dependencies:
"@auth/core" "0.41.2" "@auth/core" "0.41.2"
next-plausible@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/next-plausible/-/next-plausible-4.0.0.tgz#bc81be4f2c9ba4783f07b54d5fed5d4650c265aa"
integrity sha512-tC48VscREZ4fEvas9T4oj5qJwnpPlms0Wih1Unbgi/ozG08yN1w0IAPGp+/cHB8n6qzEAL5J0MlAS0FOr132jA==
next-pwa@5.6.0: next-pwa@5.6.0:
version "5.6.0" version "5.6.0"
resolved "https://registry.yarnpkg.com/next-pwa/-/next-pwa-5.6.0.tgz#f7b1960c4fdd7be4253eb9b41b612ac773392bf4" resolved "https://registry.yarnpkg.com/next-pwa/-/next-pwa-5.6.0.tgz#f7b1960c4fdd7be4253eb9b41b612ac773392bf4"
@@ -8184,6 +8262,11 @@ path-type@^4.0.0:
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
peek-readable@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/peek-readable/-/peek-readable-4.1.0.tgz#4ece1111bf5c2ad8867c314c81356847e8a62e72"
integrity sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==
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"
@@ -8327,6 +8410,11 @@ process-nextick-args@~2.0.0:
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"
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
process@^0.11.10:
version "0.11.10"
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==
prop-types@^15.5.4, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2: prop-types@^15.5.4, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2" version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
@@ -8555,6 +8643,17 @@ readable-stream@^3.1.1, readable-stream@^3.4.0:
string_decoder "^1.1.1" string_decoder "^1.1.1"
util-deprecate "^1.0.1" util-deprecate "^1.0.1"
readable-stream@^4.3.0, readable-stream@^4.7.0:
version "4.7.0"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.7.0.tgz#cedbd8a1146c13dfff8dab14068028d58c15ac91"
integrity sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==
dependencies:
abort-controller "^3.0.0"
buffer "^6.0.3"
events "^3.3.0"
process "^0.11.10"
string_decoder "^1.3.0"
readable-stream@~1.0.17, readable-stream@~1.0.27-1: readable-stream@~1.0.17, readable-stream@~1.0.27-1:
version "1.0.34" version "1.0.34"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
@@ -8565,6 +8664,13 @@ readable-stream@~1.0.17, readable-stream@~1.0.27-1:
isarray "0.0.1" isarray "0.0.1"
string_decoder "~0.10.x" string_decoder "~0.10.x"
readable-web-to-node-stream@^3.0.0, readable-web-to-node-stream@^3.0.2:
version "3.0.4"
resolved "https://registry.yarnpkg.com/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.4.tgz#392ba37707af5bf62d725c36c1b5d6ef4119eefc"
integrity sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==
dependencies:
readable-stream "^4.7.0"
redent@^4.0.0: redent@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/redent/-/redent-4.0.0.tgz#0c0ba7caabb24257ab3bb7a4fd95dd1d5c5681f9" resolved "https://registry.yarnpkg.com/redent/-/redent-4.0.0.tgz#0c0ba7caabb24257ab3bb7a4fd95dd1d5c5681f9"
@@ -9397,7 +9503,7 @@ string.prototype.trimstart@^1.0.8:
define-properties "^1.2.1" define-properties "^1.2.1"
es-object-atoms "^1.0.0" es-object-atoms "^1.0.0"
string_decoder@^1.1.1: string_decoder@^1.1.1, string_decoder@^1.3.0:
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
@@ -9485,6 +9591,14 @@ strip-json-comments@~2.0.1:
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
strtok3@^6.2.4, strtok3@^6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/strtok3/-/strtok3-6.3.0.tgz#358b80ffe6d5d5620e19a073aa78ce947a90f9a0"
integrity sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==
dependencies:
"@tokenizer/token" "^0.3.0"
peek-readable "^4.1.0"
styled-jsx@5.1.6: styled-jsx@5.1.6:
version "5.1.6" version "5.1.6"
resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.1.6.tgz#83b90c077e6c6a80f7f5e8781d0f311b2fe41499" resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.1.6.tgz#83b90c077e6c6a80f7f5e8781d0f311b2fe41499"
@@ -9688,6 +9802,14 @@ toidentifier@1.0.0:
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
token-types@^4.1.1, token-types@^4.2.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/token-types/-/token-types-4.2.1.tgz#0f897f03665846982806e138977dbe72d44df753"
integrity sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==
dependencies:
"@tokenizer/token" "^0.3.0"
ieee754 "^1.2.1"
tr46@^1.0.1: tr46@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09"