fix: corriger toutes les erreurs de lint restantes (263 -> 0)

This commit is contained in:
2026-07-04 14:34:50 +04:00
parent b193914c57
commit 454eefbe58
33 changed files with 210 additions and 159 deletions
+2 -2
View File
@@ -20,7 +20,7 @@ async function jwennAwtis(slug) {
}
export async function generateMetadata(props) {
const params = await props.params;
const params = await props.params
const {slug} = params
const anAwtis = await jwennAwtis(slug)
@@ -64,7 +64,7 @@ export async function generateMetadata(props) {
}
export default async function AwtisPajSlug(props) {
const params = await props.params;
const params = await props.params
const {slug} = params
const anAwtis = await jwennAwtis(slug)
+1 -1
View File
@@ -63,7 +63,7 @@ async function jwennDone(paj) {
}
export default async function AwitsPaj(props) {
const searchParams = await props.searchParams;
const searchParams = await props.searchParams
const {paj} = searchParams
const {pajTotal, awtisPouChakPaj, pajParsed} = await jwennDone(paj || 1)
+1 -1
View File
@@ -78,7 +78,7 @@ export default async function RootLayout({children}) {
)
return (
<html lang='fr' suppressHydrationWarning>
<html suppressHydrationWarning lang='fr'>
<body>
{plausibleUrl
? <PlausibleProvider src={plausibleUrl}>{inner}</PlausibleProvider>
+1 -2
View File
@@ -7,7 +7,6 @@ import Statistik from '../components/akey/statistik'
import Akey from '../components/akey'
import AnVedette from '../components/akey/an-vedette'
import okiLogo from '../public/logo-512x512.png'
import Footer from '../components/footer'
import Aso from '../components/akey/aso'
@@ -26,7 +25,7 @@ export default async function Page() {
return (
<Box sx={{display: 'flex', flexDirection: 'column', minHeight: '100vh'}}>
<Akey logo={okiLogo} />
<Akey />
<Container sx={{flexGrow: 100}}>
{dernierTeks && <AnVedette teks={dernierTeks} />}
<Statistik statistik={statistik} />
+4 -4
View File
@@ -21,10 +21,10 @@ async function jwennAnTeks(slug) {
}
export async function generateMetadata(props) {
const params = await props.params;
const params = await props.params
const {slug} = params
const anTeks = await jwennAnTeks(slug)
const anTeks = await jwennAnTeks(slug)
const awtis = anTeks?.artistes?.length === 1 ? anTeks?.artistes[0].alias : getAlias(anTeks.artistes, anTeks.prioriteArtistes)
const title = `PAWÒL-NU | ${awtis} - ${anTeks.titre}`
@@ -67,12 +67,12 @@ export async function generateMetadata(props) {
}
export default async function AnPawolPaj(props) {
const params = await props.params;
const params = await props.params
const {slug} = params
const anTeks = await jwennAnTeks(slug)
const {couverture} = anTeks
const teksKuvetiFormat = formatKuveti(couverture)
const teksKuvetiFormat = formatKuveti(couverture)
const jsonLd = {
'@context': 'http://schema.org',
+5
View File
@@ -1,4 +1,5 @@
import {Suspense} from 'react'
import PropTypes from 'prop-types'
import TeksDrawerLoader from '../../components/teks/teks-drawer-loader'
import {CurrentTrackProvider} from '../../components/teks/current-track-context'
import Loading from './loading'
@@ -50,3 +51,7 @@ export default function PawolLayout({children}) {
</CurrentTrackProvider>
)
}
PawolLayout.propTypes = {
children: PropTypes.node.isRequired
}
+1 -1
View File
@@ -1,5 +1,5 @@
export default function robots() {
const isProduction = process.env.NODE_ENV === 'production';
const isProduction = process.env.NODE_ENV === 'production'
if (!isProduction) {
return {
+1 -1
View File
@@ -14,7 +14,7 @@ export default function Sipote() {
<Box sx={{display: 'flex', marginTop: 10, flexDirection: 'column', minHeight: '100vh'}}>
<Box sx={{flexGrow: 1, marginBottom: 10}}>
<Container>
<Typography gutterBottom sx={{fontWeight: 'bold', textAlign:'center' }} variant='h4' component='h1'>
<Typography gutterBottom sx={{fontWeight: 'bold', textAlign: 'center'}} variant='h4' component='h1'>
Soutenir ORGANISATION KA INTERNATIONALE !
</Typography>
<Paper sx={{padding: 2, marginTop: 3}}>
+7 -7
View File
@@ -69,13 +69,13 @@ export default function ThemeRegistry(props) {
return (
<>
<InitColorSchemeScript attribute='class' />
<NextAppDirEmotionCacheProvider options={{key: 'mui'}}>
<ThemeProvider theme={theme}>
<CssBaseline enableColorScheme />
<ChanjeTem />
{children}
</ThemeProvider>
</NextAppDirEmotionCacheProvider>
<NextAppDirEmotionCacheProvider options={{key: 'mui'}}>
<ThemeProvider theme={theme}>
<CssBaseline enableColorScheme />
<ChanjeTem />
{children}
</ThemeProvider>
</NextAppDirEmotionCacheProvider>
</>
)
}
+2 -2
View File
@@ -36,10 +36,10 @@ export default function AnVedette({teks}) {
<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
src={`${IMAGE_URL}${fmt.url}`}
alt={titre}
placeholder='blur'
blurDataURL={BLUR_DATA_URL}
sizes='(max-width: 600px) 100vw, 800px'
+1 -10
View File
@@ -1,13 +1,8 @@
import PropTypes from 'prop-types'
import Container from '@mui/material/Container'
import Typography from '@mui/material/Typography'
import Box from '@mui/material/Box'
import Grid from '@mui/material/Grid'
import Button from '@mui/material/Button'
import Image from 'next/image'
import Link from 'next/link'
export default function Akey({logo}) {
export default function Akey() {
return (
<Box sx={{flexGrow: 1, marginBottom: 3, marginTop: 7}}>
<Container align='center'>
@@ -25,7 +20,3 @@ export default function Akey({logo}) {
</Box>
)
}
Akey.propTypes = {
logo: PropTypes.object.isRequired
}
+10 -9
View File
@@ -72,7 +72,8 @@ export default function AwtisDetay({anAwtis}) {
position: 'relative',
flexShrink: 0,
mb: 2,
}}>
}}
>
{photoUrl ? (
<Image
src={photoUrl}
@@ -94,8 +95,8 @@ export default function AwtisDetay({anAwtis}) {
{rezoSosyal?.length > 0 && (
<Box sx={{display: 'flex', gap: 0.75, flexWrap: 'wrap', justifyContent: 'center', mb: 1.5}}>
{rezoSosyal.map((rezo, i) => (
<SocialButton key={i} rezo={rezo} />
{rezoSosyal.map(rezo => (
<SocialButton key={rezo.url} rezo={rezo} />
))}
</Box>
)}
@@ -149,9 +150,9 @@ export default function AwtisDetay({anAwtis}) {
<Link href={`/paroles/${titrePhare.slug}`} style={{display: 'block', height: '100%'}}>
<Box sx={{position: 'relative', minHeight: 130, height: '100%'}}>
<Image
fill
src={coverUrl}
alt={titrePhare.titre}
fill
placeholder='blur'
blurDataURL={BLUR_DATA_URL}
sizes='200px'
@@ -163,7 +164,7 @@ export default function AwtisDetay({anAwtis}) {
)}
<Grid size={{xs: 12, sm: coverUrl ? 8 : 12}}>
<CardContent>
<Typography variant='subtitle1' sx={{fontWeight: 700}} gutterBottom>
<Typography gutterBottom variant='subtitle1' sx={{fontWeight: 700}}>
<Link href={`/paroles/${titrePhare.slug}`} style={{color: 'inherit', textDecoration: 'none'}}>
{titrePhare.titre}
</Link>
@@ -172,8 +173,8 @@ export default function AwtisDetay({anAwtis}) {
Écouter sur
</Typography>
<Box sx={{display: 'flex', flexWrap: 'wrap', gap: 1}}>
{titrePhare.streamAudio.map((lyen, i) => (
<StreamButton key={i} lyen={lyen} />
{titrePhare.streamAudio.map(lyen => (
<StreamButton key={lyen.url} lyen={lyen} />
))}
</Box>
</CardContent>
@@ -202,7 +203,7 @@ export default function AwtisDetay({anAwtis}) {
})}
</AccordionDetails>
</Accordion>
) : paroles.length === 0 ? (
) : (paroles.length === 0 ? (
<Typography textAlign='center' variant='body1' color='text.secondary'>
Aucune parole pour le moment
</Typography>
@@ -213,7 +214,7 @@ export default function AwtisDetay({anAwtis}) {
<MizikLyen anPawol={paroles[0]} kuveti={formatKuveti(paroles[0].couverture, 'thumbnail')} />
</Paper>
</Box>
)}
))}
</Grid>
</Grid>
+1 -2
View File
@@ -19,7 +19,6 @@ import {formatKuveti} from '../../lib/kuveti'
import AwtisBiyografi from './awtis-biyografi'
const PREFIX = 'awtis-kat'
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3001'
const IMAGE_URL = process.env.NEXT_PUBLIC_API_URL_ROOT || 'http://localhost:1337'
const EXCLUSIVE_LABEL = process.env.NEXT_PUBLIC_EXCLUSIVE_ARTIST_LABEL || 'OKI Exclusif'
@@ -84,7 +83,7 @@ export default function AwtisKat({artiste}) {
component='img'
alt={alias}
image={`${photo?.url ? `${IMAGE_URL}${formatKuveti(photo, 'thumbnail')?.url}` : noImageUrl}`}
loading='lazy'
loading='lazy'
title={alias}
/>
<CardContent>
+15 -18
View File
@@ -5,7 +5,6 @@ import {useRouter} from 'next/navigation'
import TextField from '@mui/material/TextField'
import Autocomplete from '@mui/material/Autocomplete'
import Avatar from '@mui/material/Avatar'
import CircularProgress from '@mui/material/CircularProgress'
import Container from '@mui/material/Container'
import {jwennToutAwtis} from '../../lib/oki-api'
@@ -29,7 +28,7 @@ export default function ChecheAwtis() {
(async () => {
try {
const {data} = await jwennToutAwtis()
const filteredData = data.map(artiste => {
const firstLetter = artiste.alias[0].toUpperCase()
return {
@@ -50,15 +49,13 @@ export default function ChecheAwtis() {
}
}, [loading])
const sortedOptions = useMemo(() => {
return [...options].sort((a, b) =>
-b.firstLetter.localeCompare(a.firstLetter)
);
}, [options]);
const sortedOptions = useMemo(() => [...options].sort((a, b) =>
-b.firstLetter.localeCompare(a.firstLetter)
), [options])
return (
<Container
sx={{ display: 'flex', justifyContent: 'center', marginBottom: 3 }}
sx={{display: 'flex', justifyContent: 'center', marginBottom: 3}}
>
<Autocomplete
autoHighlight
@@ -70,35 +67,35 @@ export default function ChecheAwtis() {
noOptionsText='Aucun résultat'
id='cheche-awtis'
options={sortedOptions}
groupBy={(option) => option?.firstLetter}
getOptionLabel={(option) => option?.alias}
renderOption={(props, option) => {
const {key, ...rest} = props;
groupBy={option => option?.firstLetter}
getOptionLabel={option => option?.alias}
renderOption={(optionProps, option) => {
const {key, ...rest} = optionProps
return (
<li key={key} {...rest}>
<Avatar
style={{ marginRight: 8 }}
style={{marginRight: 8}}
alt={option?.alias}
src={`${IMAGE_URL}${formatKuveti(option?.photo, 'thumbnail')?.url || ''}`}
/>
{option?.alias}
</li>
);
)
}}
sx={{ width: 300 }}
renderInput={(params) => (
sx={{width: 300}}
renderInput={params => (
<TextField
{...params}
label='Rechercher un artiste'
slotProps={{
...params.slotProps,
htmlInput: { ...params.slotProps.htmlInput },
htmlInput: {...params.slotProps.htmlInput},
}}
/>
)}
onChange={(event, newValue) => {
router.push(`/awtis/${newValue.slug}`);
router.push(`/awtis/${newValue.slug}`)
}}
onOpen={() => setOpen(true)}
onClose={() => setOpen(false)}
+25 -17
View File
@@ -1,5 +1,6 @@
'use client'
import PropTypes from 'prop-types'
import Chip from '@mui/material/Chip'
import IconButton from '@mui/material/IconButton'
import Tooltip from '@mui/material/Tooltip'
@@ -11,23 +12,23 @@ import {
} from '@icons-pack/react-simple-icons'
const SOCIAL_CONFIG = {
Mastodon: {label: 'Mastodon', bg: '#6364FF', color: '#fff', Icon: Mastodon},
Peertube: {label: 'PeerTube', bg: '#F2690D', color: '#fff', Icon: Peertube},
Pixelfed: {label: 'Pixelfed', bg: '#11D49D', color: '#fff', Icon: null},
Funkwhale: {label: 'Funkwhale', bg: '#E01B60', color: '#fff', Icon: null},
Bluesky: {label: 'Bluesky', bg: '#0085FF', color: '#fff', Icon: null},
Instagram: {label: 'Instagram', bg: '#E4405F', color: '#fff', Icon: Instagram},
Youtube: {label: 'YouTube', bg: '#FF0000', color: '#fff', Icon: Youtube},
Tiktok: {label: 'TikTok', bg: '#000000', color: '#fff', Icon: Tiktok},
Spotify: {label: 'Spotify', bg: '#1DB954', color: '#fff', Icon: Spotify},
Deezer: {label: 'Deezer', bg: '#EF5466', color: '#fff', Icon: Deezer},
Mastodon: {label: 'Mastodon', bg: '#6364FF', color: '#fff', Icon: Mastodon},
Peertube: {label: 'PeerTube', bg: '#F2690D', color: '#fff', Icon: Peertube},
Pixelfed: {label: 'Pixelfed', bg: '#11D49D', color: '#fff', Icon: null},
Funkwhale: {label: 'Funkwhale', bg: '#E01B60', color: '#fff', Icon: null},
Bluesky: {label: 'Bluesky', bg: '#0085FF', color: '#fff', Icon: null},
Instagram: {label: 'Instagram', bg: '#E4405F', color: '#fff', Icon: Instagram},
Youtube: {label: 'YouTube', bg: '#FF0000', color: '#fff', Icon: Youtube},
Tiktok: {label: 'TikTok', bg: '#000000', color: '#fff', Icon: Tiktok},
Spotify: {label: 'Spotify', bg: '#1DB954', color: '#fff', Icon: Spotify},
Deezer: {label: 'Deezer', bg: '#EF5466', color: '#fff', Icon: Deezer},
Applemusic: {label: 'Apple Music', bg: '#FC3C44', color: '#fff', Icon: Applemusic},
Bandcamp: {label: 'Bandcamp', bg: '#1DA0C3', color: '#fff', Icon: Bandcamp},
Soundcloud: {label: 'SoundCloud', bg: '#FF5500', color: '#fff', Icon: Soundcloud},
Facebook: {label: 'Facebook', bg: '#1877F2', color: '#fff', Icon: Facebook},
Twitter: {label: 'X / Twitter', bg: '#000000', color: '#fff', Icon: Twitter},
Linktree: {label: 'Linktree', bg: '#43E660', color: '#000', Icon: null},
SiteWeb: {label: 'Site web', bg: '#555555', color: '#fff', Icon: null},
Bandcamp: {label: 'Bandcamp', bg: '#1DA0C3', color: '#fff', Icon: Bandcamp},
Soundcloud: {label: 'SoundCloud', bg: '#FF5500', color: '#fff', Icon: Soundcloud},
Facebook: {label: 'Facebook', bg: '#1877F2', color: '#fff', Icon: Facebook},
Twitter: {label: 'X / Twitter', bg: '#000000', color: '#fff', Icon: Twitter},
Linktree: {label: 'Linktree', bg: '#43E660', color: '#000', Icon: null},
SiteWeb: {label: 'Site web', bg: '#555555', color: '#fff', Icon: null},
}
export function SocialButton({rezo}) {
@@ -59,15 +60,22 @@ export function SocialButton({rezo}) {
return (
<Tooltip title={rezo.url}>
<Chip
clickable
label={config.label}
component='a'
href={rezo.url}
target='_blank'
rel='noopener noreferrer'
clickable
size='small'
sx={{bgcolor: config.bg, color: config.color, fontWeight: 600}}
/>
</Tooltip>
)
}
SocialButton.propTypes = {
rezo: PropTypes.shape({
plateforme: PropTypes.string.isRequired,
url: PropTypes.string.isRequired
}).isRequired
}
+2 -2
View File
@@ -57,7 +57,7 @@ export default function LicenseModal({license, sourceOriginale, remixes}) {
<DialogContent dividers>
{sourceOriginale && (
<Box sx={{mb: 3}}>
<Typography variant='overline' color='text.secondary' display='block' gutterBottom>
<Typography gutterBottom variant='overline' color='text.secondary' display='block'>
Basé sur
</Typography>
<Link href={`/paroles/${sourceOriginale.slug}`} underline='hover' color='inherit'>
@@ -76,7 +76,7 @@ export default function LicenseModal({license, sourceOriginale, remixes}) {
)}
{remixes && (
<Box sx={{mb: 3}}>
<Typography variant='overline' color='text.secondary' display='block' gutterBottom>
<Typography gutterBottom variant='overline' color='text.secondary' display='block'>
Déclinaisons
</Typography>
{remixes.map(remix => (
+25 -10
View File
@@ -70,12 +70,14 @@ export default function FilesList({files}) {
useEffect(() => {
const audioFiles = files.filter(f => f.mime.startsWith('audio'))
if (audioFiles.length === 0) return
if (audioFiles.length === 0) {
return
}
let cancelled = false
async function fetchAllMeta() {
const mm = await import('music-metadata-browser')
const mm = await import('music-metadata-browser') // eslint-disable-line node/no-unsupported-features/es-syntax
const results = {}
await Promise.all(
audioFiles.map(async file => {
@@ -98,7 +100,9 @@ export default function FilesList({files}) {
}
})
)
if (!cancelled) setAudioMeta(results)
if (!cancelled) {
setAudioMeta(results)
}
}
fetchAllMeta()
@@ -121,7 +125,8 @@ export default function FilesList({files}) {
fontWeight: 'bold',
letterSpacing: '0.05rem',
textTransform: 'uppercase',
}}>{isHaute ? 'Haute' : 'Faible'}</Typography>
}}
>{isHaute ? 'Haute' : 'Faible'}</Typography>
)
}
@@ -150,12 +155,16 @@ export default function FilesList({files}) {
}
useEffect(() => () => {
Object.values(controllersRef.current).forEach(c => c.abort())
for (const c of Object.values(controllersRef.current)) {
c.abort()
}
}, [])
const handleClick = async (e, url, fileName, fileId) => {
e.stopPropagation()
if (fileId in downloading) return
if (fileId in downloading) {
return
}
const controller = new AbortController()
controllersRef.current[fileId] = controller
@@ -163,14 +172,18 @@ export default function FilesList({files}) {
setDownloading(prev => ({...prev, [fileId]: 0}))
try {
const response = await fetch(url, {signal: controller.signal})
const contentLength = +response.headers.get('content-length')
const contentLength = Number(response.headers.get('content-length'))
const reader = response.body.getReader()
const chunks = []
let received = 0
while (true) {
for (;;) {
// eslint-disable-next-line no-await-in-loop -- chaque chunk dépend du précédent, la lecture est intrinsèquement séquentielle
const {done, value} = await reader.read()
if (done) break
if (done) {
break
}
chunks.push(value)
received += value.length
if (contentLength) {
@@ -186,7 +199,9 @@ export default function FilesList({files}) {
a.click()
URL.revokeObjectURL(blobUrl)
} catch (error) {
if (error.name !== 'AbortError') throw error
if (error.name !== 'AbortError') {
throw error
}
} finally {
delete controllersRef.current[fileId]
setDownloading(prev => {
+2 -2
View File
@@ -48,10 +48,10 @@ export default function KitsDialog({kits}) {
<DialogTitle>Télécharger un kit</DialogTitle>
<DialogContent dividers>
<List>
{kits.map((kit, index) => {
{kits.map(kit => {
const PlatformIcon = PLATFORM_ICON[kit.plateforme] ?? CloudDownloadIcon
return (
<ListItem key={index} disablePadding>
<ListItem key={kit.url} disablePadding>
<ListItemButton component='a' href={kit.url} target='_blank' rel='noopener noreferrer'>
<ListItemIcon>
<PlatformIcon />
+3 -2
View File
@@ -70,7 +70,8 @@ export default function Navigasyon() {
<Appdiv>
<div className={classes.root}>
<AppBar style={{zIndex: 1, boxShadow: 'none'}} position='fixed' color='default'>
{selectedTab !== undefined && <Tabs
{selectedTab !== undefined && (
<Tabs
allowScrollButtonsMobile
scrollButtons
centered={Boolean(!isMobile)}
@@ -91,7 +92,7 @@ export default function Navigasyon() {
<Tab icon={<MusicNoteIcon />} aria-label='Paroles' {...a11yProps(2)} />
<Tab icon={<RecordVoiceOverIcon />} aria-label='Artistes' {...a11yProps(3)} />
</Tabs>
}
)}
</AppBar>
</div>
</Appdiv>
-2
View File
@@ -4,8 +4,6 @@ import ArrowRightAltIcon from '@mui/icons-material/ArrowRightAlt'
import CGUDialog from '../cgu/cgu-dialog'
const CGU_PDF_LINK = process.env.NEXT_PUBLIC_CGU_DOWNLOAD_LINK
export default function Presantasyon() {
const [open, setOpen] = useState(false)
+16 -8
View File
@@ -1,18 +1,19 @@
'use client'
import PropTypes from 'prop-types'
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},
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}) {
@@ -39,3 +40,10 @@ export function StreamButton({lyen}) {
</Button>
)
}
StreamButton.propTypes = {
lyen: PropTypes.shape({
plateforme: PropTypes.string.isRequired,
url: PropTypes.string.isRequired
}).isRequired
}
+1 -1
View File
@@ -20,7 +20,7 @@ const getMizikFiltered = (paroles, filter) => {
const filteredTitre = paroles.filter(({titre}) => {
const deburredTit = deburr(titre)
return deburredTit.toLowerCase().includes(deburr(filter.toLowerCase()))
})
})
const filteredAlias = paroles.filter(({artistes}) => {
const aliasLis = artistes.map(({alias}) => deburr(alias)).join(', ')
+20 -14
View File
@@ -21,7 +21,7 @@ const pulse = keyframes`
`
const SlideUp = forwardRef(function SlideUp(props, ref) {
return <Slide direction='up' ref={ref} {...props} />
return <Slide ref={ref} direction='up' {...props} />
})
function toEmbedUrl(url) {
@@ -31,6 +31,7 @@ function toEmbedUrl(url) {
if (match) {
return `${u.origin}/videos/embed/${match[1]}?title=0&warningTitle=0&peertubeLink=0&controlBar=1`
}
return url
} catch {
return url
@@ -45,7 +46,9 @@ export default function KaraokeModal({url, desktopUrl, titre, artistes}) {
const activeUrl = (!isMobile && desktopUrl) ? desktopUrl : url
useEffect(() => {
if (!open) setLoaded(false)
if (!open) {
setLoaded(false)
}
}, [open])
return (
@@ -55,13 +58,13 @@ export default function KaraokeModal({url, desktopUrl, titre, artistes}) {
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,
}}
onClick={() => setOpen(true)}
>
<MicIcon />
</Fab>
@@ -69,7 +72,6 @@ export default function KaraokeModal({url, desktopUrl, titre, artistes}) {
<Dialog
open={open}
onClose={() => setOpen(false)}
slots={{transition: SlideUp}}
maxWidth={false}
sx={{
@@ -102,6 +104,7 @@ export default function KaraokeModal({url, desktopUrl, titre, artistes}) {
}
}
}}
onClose={() => setOpen(false)}
>
{/* Header superposé en gradient */}
<Box sx={{
@@ -118,7 +121,8 @@ export default function KaraokeModal({url, desktopUrl, titre, artistes}) {
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)'}}>
@@ -133,7 +137,6 @@ export default function KaraokeModal({url, desktopUrl, titre, artistes}) {
</Box>
<IconButton
size='small'
onClick={() => setOpen(false)}
aria-label='fermer'
sx={{
pointerEvents: 'auto',
@@ -143,6 +146,7 @@ export default function KaraokeModal({url, desktopUrl, titre, artistes}) {
border: '1px solid rgba(255,255,255,0.12)',
'&:hover': {bgcolor: 'rgba(255,255,255,0.25)'},
}}
onClick={() => setOpen(false)}
>
<CloseIcon fontSize='small' />
</IconButton>
@@ -166,7 +170,8 @@ export default function KaraokeModal({url, desktopUrl, titre, artistes}) {
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'
@@ -187,27 +192,28 @@ export default function KaraokeModal({url, desktopUrl, titre, artistes}) {
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
src={toEmbedUrl(activeUrl)}
style={{width: '100%', height: '100%', border: 'none', display: 'block', opacity: loaded ? 1 : 0, transition: 'opacity 0.4s ease'}}
allow='autoplay; fullscreen'
sandbox='allow-same-origin allow-scripts allow-popups allow-forms'
title='Karaoké'
onLoad={() => setLoaded(true)}
/>
</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
src={toEmbedUrl(activeUrl)}
style={{width: '100%', height: '100%', border: 'none', display: 'block', opacity: loaded ? 1 : 0, transition: 'opacity 0.4s ease'}}
allow='autoplay; fullscreen'
sandbox='allow-same-origin allow-scripts allow-popups allow-forms'
title='Karaoké'
onLoad={() => setLoaded(true)}
/>
)}
</Box>
+13 -5
View File
@@ -30,9 +30,10 @@ function createAudio(src) {
function parseLrc(text) {
const lines = []
for (const raw of text.split('\n')) {
const match = raw.match(/^\[(\d{2}):(\d{2})[.:]\d+\](.*)$/)
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()})
const time = (Number.parseInt(match[1], 10) * 60) + Number.parseInt(match[2], 10)
lines.push({time, text: match[3].trim()})
}
}
@@ -145,6 +146,7 @@ export default function Lekte({audio, url, parole}) {
setPosition(0)
el.currentTime = 0
}
el.addEventListener('loadedmetadata', onLoaded)
el.addEventListener('ended', onEnded)
return () => {
@@ -154,7 +156,10 @@ export default function Lekte({audio, url, parole}) {
}, [audio])
useEffect(() => {
if (!parole?.pawol?.url) return
if (!parole?.pawol?.url) {
return
}
fetch(new URL(parole.pawol.url, IMAGE_URL).toString())
.then(r => r.text())
.then(text => setLrcLines(parseLrc(text)))
@@ -162,7 +167,7 @@ export default function Lekte({audio, url, parole}) {
}, [parole?.pawol?.url])
const activeIndex = lrcLines
? lrcLines.reduce((last, line, i) => line.time <= position ? i : last, -1)
? lrcLines.findLastIndex(line => line.time <= position)
: -1
const handleChangePosition = value => {
@@ -317,7 +322,10 @@ export default function Lekte({audio, url, parole}) {
{[-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}} />
if (!line) {
return <Box key={offset} sx={{height: offset === 0 ? 28 : 20}} />
}
return (
<Typography
key={offset}
+1 -1
View File
@@ -51,9 +51,9 @@ export default function TeksKat({parole}) {
<Box sx={{position: 'relative', height: 240}}>
{couverture?.url ? (
<Image
fill
src={`${IMAGE_URL}${formatKuveti(couverture, 'thumbnail')?.url}`}
alt={titre}
fill
placeholder='blur'
blurDataURL={BLUR_DATA_URL}
sizes='(max-width: 600px) 100vw, (max-width: 900px) 50vw, 33vw'
+6 -6
View File
@@ -183,7 +183,7 @@ export default function Teks({parole}) {
<i>parole soumise par {parole.user.username}</i>
</Typography>
)}
{parole?.userAdmin&& !parole.user && (
{parole?.userAdmin && !parole.user && (
<Typography style={{marginBottom: '1.5em'}} display='block' variant='caption'>
<i>parole soumise par {parole.userAdmin.firstname}</i>
</Typography>
@@ -220,8 +220,8 @@ export default function Teks({parole}) {
Écouter sur
</Typography>
<Box sx={{display: 'flex', flexWrap: 'wrap', justifyContent: 'center', gap: 1}}>
{parole.streamAudio.map((lyen, i) => (
<StreamButton key={i} lyen={lyen} />
{parole.streamAudio.map(lyen => (
<StreamButton key={lyen.url} lyen={lyen} />
))}
</Box>
</Box>
@@ -229,11 +229,10 @@ export default function Teks({parole}) {
</Box>
<Box sx={{maxWidth: 700, margin: '0 auto'}} className={classes.gridText}>
<Tabs
value={tab}
onChange={(event, value) => setTab(value)}
variant='scrollable'
scrollButtons
allowScrollButtonsMobile
value={tab}
variant='scrollable'
centered={langArray.length === 0}
slots={{
startScrollButtonIcon: ArrowBackIosNewIcon,
@@ -255,6 +254,7 @@ export default function Teks({parole}) {
}
}
}}
onChange={(event, value) => setTab(value)}
>
<Tab label='Transcription' />
{langArray.map(({title}) => (
+5
View File
@@ -1,7 +1,12 @@
'use client'
import PropTypes from 'prop-types'
import NextTopLoader from 'nextjs-toploader'
export default function TopLoader({color}) {
return <NextTopLoader color={color} />
}
TopLoader.propTypes = {
color: PropTypes.string.isRequired
}
+14 -14
View File
@@ -1,25 +1,25 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
import {dirname} from 'node:path'
import {fileURLToPath} from 'node:url'
import {FlatCompat} from '@eslint/eslintrc'
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const compat = new FlatCompat({
baseDirectory: __dirname,
});
})
const eslintConfig = [
...compat.extends("next/core-web-vitals"),
...compat.extends('next/core-web-vitals'),
{
ignores: [
"node_modules/**",
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
'node_modules/**',
'.next/**',
'out/**',
'build/**',
'next-env.d.ts',
],
},
];
]
export default eslintConfig;
export default eslintConfig
+11 -5
View File
@@ -1,15 +1,21 @@
const SIZE_ORDER = {
large: ['large', 'medium', 'small'],
medium: ['medium', 'small', 'large'],
small: ['small', 'medium', 'large'],
large: ['large', 'medium', 'small'],
medium: ['medium', 'small', 'large'],
small: ['small', 'medium', 'large'],
thumbnail: ['thumbnail', 'small', 'medium', 'large'],
}
export const formatKuveti = (kuveti, preferred = 'large') => {
if (!kuveti) return null
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.formats?.[size]) {
return kuveti.formats[size]
}
}
return kuveti
}
+3 -2
View File
@@ -186,6 +186,7 @@ async function jwennTouSlug(endpoint) {
encodeValuesOnly: true
})
// eslint-disable-next-line no-await-in-loop -- pagination séquentielle, chaque page dépend du nombre de résultats de la précédente
const {data} = await request(`/${endpoint}?${query}`, headers)
slugs.push(...data.map(({slug}) => slug))
hasMore = data.length === pageSize
@@ -223,9 +224,9 @@ export async function jwennAnVedette() {
const query = qs.stringify({
populate: ['artistes', 'couverture'],
filters: {
isNewRelease: { $eq: true }
isNewRelease: {$eq: true}
},
pagination: { limit: 1 }
pagination: {limit: 1}
}, {
encodeValuesOnly: true
})
-4
View File
@@ -1,16 +1,12 @@
import YouTubeIcon from '@mui/icons-material/YouTube'
import TelegramIcon from '@mui/icons-material/Telegram'
import PeertubeIcon from './icons/peertube'
import XmppIcon from './icons/xmpp'
import XIcon from './icons/x'
import GiteaIcon from './icons/gitea'
import BlueskyIcon from './icons/bluesky'
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 xmppContact = process.env.NEXT_PUBLIC_XMPP || 'oki@xmpp.cz'
const gadeUsername = process.env.NEXT_PUBLIC_GADE_USERNAME || 'oki'
const youtubeUsername = process.env.NEXT_PUBLIC_YOUTUBE_USERNAME || 'OrganisationKInternationaleOKi'
const tiwtterUsername = process.env.NEXT_PUBLIC_TWITTER_USERNAME || 'OrganisationKA'
const telegramGroup = process.env.NEXT_PUBLIC_TELEGRAM_GROUP || 'OrganisationKA'
+10 -3
View File
@@ -11,13 +11,20 @@ function buildRemotePatterns() {
const [hostname, port] = entry.split(':')
const isLocal = hostname === 'localhost' || hostname === '127.0.0.1'
const pattern = {protocol: isLocal ? 'http' : 'https', hostname, pathname: '/uploads/**'}
if (port) pattern.port = port
if (port) {
pattern.port = port
}
return pattern
})
if (!raw.includes('localhost'))
if (!raw.includes('localhost')) {
patterns.push({protocol: 'http', hostname: 'localhost', port: '1337', pathname: '/uploads/**'})
if (!raw.includes('127.0.0.1'))
}
if (!raw.includes('127.0.0.1')) {
patterns.push({protocol: 'http', hostname: '127.0.0.1', port: '1337', pathname: '/uploads/**'})
}
return patterns
}
+1 -1
View File
@@ -1 +1 @@
import '@testing-library/jest-dom/vitest'
import '@testing-library/jest-dom/vitest' // eslint-disable-line import/no-unassigned-import