Compare commits

..

42 Commits

Author SHA1 Message Date
cedric 9752e4bcdd feat: redesign artist page layout and social icons
Déploiement FRONT BETA / check (push) Successful in 2m13s
Déploiement FRONT PROD / check (push) Successful in 2m15s
Déploiement FRONT BETA / deploy (push) Successful in 22s
Déploiement FRONT PROD / deploy (push) Successful in 21s
2026-06-26 22:03:19 +04:00
cedric 719522f665 Merge pull request 'feat: add social networks to artist page' (#3) from feat/improve-artist-page into master
Déploiement FRONT BETA / check (push) Successful in 2m7s
Déploiement FRONT PROD / check (push) Successful in 2m12s
Déploiement FRONT BETA / deploy (push) Successful in 20s
Déploiement FRONT PROD / deploy (push) Successful in 20s
Reviewed-on: #3
2026-06-26 17:37:45 +00:00
cedric 1c86d0b962 fix: use available icon exports in social-buttons
Déploiement FRONT BETA / check (push) Successful in 2m10s
Déploiement FRONT BETA / deploy (push) Successful in 22s
Vérification PR / check (pull_request) Successful in 2m2s
Vérification PR / deploy-beta (pull_request) Successful in 19s
2026-06-26 21:16:57 +04:00
cedric 96dafb7b54 feat: populate and display rezoSosyal on artist page
Déploiement FRONT BETA / check (push) Successful in 2m12s
Déploiement FRONT BETA / deploy (push) Successful in 23s
2026-06-26 12:23:49 +04:00
cedric d64aa863dc feat: add SocialButton component for artist social networks 2026-06-26 12:23:44 +04:00
cedric 7b0c01ce06 docs: generate CGU with script
Déploiement FRONT BETA / check (push) Successful in 2m4s
Déploiement FRONT PROD / check (push) Successful in 2m5s
Déploiement FRONT BETA / deploy (push) Successful in 21s
Déploiement FRONT PROD / deploy (push) Successful in 20s
2026-06-26 10:07:38 +04:00
cedric 86998dca42 feat: add CGU markdown source and pdf generation script 2026-06-26 10:07:00 +04:00
cedric e3366dd821 feat: add CGU pdf for 26/06/2026
Déploiement FRONT BETA / check (push) Successful in 1m58s
Déploiement FRONT PROD / check (push) Successful in 2m13s
Déploiement FRONT BETA / deploy (push) Successful in 21s
Déploiement FRONT PROD / deploy (push) Successful in 20s
2026-06-26 10:01:36 +04:00
cedric 475cbe9028 fix: update réalisation contact info in CGU 2026-06-26 09:56:51 +04:00
cedric 288d771075 Merge pull request 'feat: rendre le dépôt configurable via variables d'env' (#2) from feat/improve-custom into master
Déploiement FRONT BETA / check (push) Successful in 2m6s
Déploiement FRONT PROD / check (push) Successful in 2m2s
Déploiement FRONT BETA / deploy (push) Successful in 21s
Déploiement FRONT PROD / deploy (push) Successful in 22s
Reviewed-on: #2
2026-06-26 05:14:10 +00:00
cedric cd5ebb8058 ci: deploy beta on all branches including master
Déploiement FRONT BETA / check (push) Successful in 2m6s
Vérification PR / check (pull_request) Successful in 2m8s
Déploiement FRONT BETA / deploy (push) Successful in 20s
Vérification PR / deploy-beta (pull_request) Successful in 19s
2026-06-26 07:26:30 +04:00
cedric aa3032d25e ci: checkout correct branch before deploy
Déploiement FRONT BETA / check (push) Successful in 2m2s
Vérification PR / check (pull_request) Successful in 2m16s
Déploiement FRONT BETA / deploy (push) Successful in 43s
Vérification PR / deploy-beta (pull_request) Successful in 22s
2026-06-26 07:14:44 +04:00
cedric cd24d90b67 ci: deploy beta on any branch except master 2026-06-26 07:13:02 +04:00
cedric 5c16f126a7 docs: update .env.sample with new variables
Vérification PR / check (pull_request) Successful in 2m11s
Vérification PR / deploy-beta (pull_request) Successful in 27s
2026-06-26 00:34:20 +04:00
cedric 69535aa3f4 refactor: rename isOKIAwtis to isExclusiveArtist 2026-06-26 00:34:14 +04:00
cedric ce8053a3f6 feat: redirect to PDF when NEXT_PUBLIC_CGU_DOWNLOAD_LINK is set 2026-06-26 00:34:09 +04:00
cedric 9cbb5e3d23 feat: extract hardcoded branding values to env vars 2026-06-26 00:34:04 +04:00
cedric f2d03ebec6 feat: parse image domains from NEXT_PUBLIC_DOMAINS_IMAGE 2026-06-26 00:33:54 +04:00
cedric 2232cd7360 feat: replace static manifest.json with dynamic app/manifest.js 2026-06-26 00:33:50 +04:00
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
37 changed files with 1182 additions and 358 deletions
+25 -2
View File
@@ -6,6 +6,16 @@ SITE_URL=http://localhost:3001
NEXT_PUBLIC_READ_TOKEN= NEXT_PUBLIC_READ_TOKEN=
NEXT_PUBLIC_ADMIN_JWT_SECRET= NEXT_PUBLIC_ADMIN_JWT_SECRET=
# IDENTITÉ DU SITE (branding)
NEXT_PUBLIC_SITE_NAME=PAWÒL-NU. Paroles et traductions.
NEXT_PUBLIC_SITE_SHORT_NAME=PAWÒL-NU
NEXT_PUBLIC_SITE_DESCRIPTION=PAWÒL-NU a pour but de promouvoir le Medukam (ou Wanni Wannan) et les productions afro-diasporiques.
NEXT_PUBLIC_ORG_NAME=OKI
NEXT_PUBLIC_ORG_FULL_NAME=ORGANISATION KA INTERNATIONALE
NEXT_PUBLIC_ORG_EMAIL=kontak@o-k-i.net
NEXT_PUBLIC_ORG_LOCATION=Guadeloupe
NEXT_PUBLIC_EXCLUSIVE_ARTIST_LABEL=OKI Exclusif
# FUNKWHALE VARIABLE # FUNKWHALE VARIABLE
NEXT_PUBLIC_OKI_MIZIK_URL=https://funkwhale-server.com NEXT_PUBLIC_OKI_MIZIK_URL=https://funkwhale-server.com
NEXT_PUBLIC_MIZIK_API_USER=user NEXT_PUBLIC_MIZIK_API_USER=user
@@ -14,9 +24,11 @@ NEXT_PUBLIC_MIZIK_API_PASSWORD=password
NEXT_PUBLIC_AWTIS_POU_CHAK_PAJ=6 NEXT_PUBLIC_AWTIS_POU_CHAK_PAJ=6
NEXT_PUBLIC_SITE_URL=$SITE_URL NEXT_PUBLIC_SITE_URL=$SITE_URL
# NEXT AUTH # Auth
NEXTAUTH_URL=http://localhost:3000 NEXTAUTH_URL=http://localhost:3000
BETTER_AUTH_URL=http://localhost:3000
BETTER_AUTH_SECRET=
# TWITTER VARIABLE # TWITTER VARIABLE
@@ -30,7 +42,13 @@ GOOGLE_CLIENT_SECRET=
# CGU # CGU
NEXT_PUBLIC_CGU_DOWNLOAD_LINK= NEXT_PUBLIC_CGU_DOWNLOAD_LINK=/cgu/cgu-confidentialite-oki-26-06-2026.pdf
# Debug (mettre YES pour afficher les emails dans les logs en développement)
SHOW_EMAILS=
# Analytique (Plausible — laisser vide pour désactiver)
NEXT_PUBLIC_PLAUSIBLE_URL=
# Menu # Menu
@@ -62,6 +80,8 @@ NEXT_PUBLIC_YOUTUBE_USERNAME=
NEXT_PUBLIC_TELEGRAM_GROUP= NEXT_PUBLIC_TELEGRAM_GROUP=
NEXT_PUBLIC_XMPP= NEXT_PUBLIC_XMPP=
NEXT_PUBLIC_GIT= NEXT_PUBLIC_GIT=
NEXT_PUBLIC_CODEBERG=
NEXT_PUBLIC_BLUESKY_URL=
# DOMAIN IMAGE # DOMAIN IMAGE
NEXT_PUBLIC_DOMAINS_IMAGE="localhost:1337 strapi.mondomaine.com" NEXT_PUBLIC_DOMAINS_IMAGE="localhost:1337 strapi.mondomaine.com"
@@ -85,8 +105,11 @@ SMTP_PASSWORD=
SMTP_FROM= SMTP_FROM=
SMTP_REPLY_TO= SMTP_REPLY_TO=
SMTP_SEND_TO= SMTP_SEND_TO=
SMTP_BCC=
SMTP_SECURE=
# Twitter Authentification # Twitter Authentification
NEXT_PUBLIC_TWITTER_API_KEY= NEXT_PUBLIC_TWITTER_API_KEY=
NEXT_PUBLIC_TWITTER_API_KEY_SECRET= NEXT_PUBLIC_TWITTER_API_KEY_SECRET=
NEXT_PUBLIC_TWITTER_BEARER_TOKEN=
+2 -3
View File
@@ -2,8 +2,6 @@ name: Déploiement FRONT BETA
run-name: ${{ gitea.actor }} déploie FRONT BETA run-name: ${{ gitea.actor }} déploie FRONT BETA
on: on:
push: push:
branches:
- dev
jobs: jobs:
check: check:
@@ -36,7 +34,8 @@ jobs:
export NVM_DIR="$HOME/.nvm" export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
cd ${{ secrets.FRONT_DEPLOY_PATH }} cd ${{ secrets.FRONT_DEPLOY_PATH }}
git pull --ff-only origin dev git fetch origin
git checkout -B ${{ gitea.ref_name }} origin/${{ gitea.ref_name }}
corepack enable corepack enable
yarn install --frozen-lockfile yarn install --frozen-lockfile
yarn build yarn build
+2 -2
View File
@@ -50,11 +50,11 @@ export async function generateMetadata(props) {
type: 'website' type: 'website'
}, },
twitter: { twitter: {
site: '@OrganisationKA', site: `@${process.env.NEXT_PUBLIC_TWITTER_USERNAME || 'OrganisationKA'}`,
card: 'summary_large_image', card: 'summary_large_image',
title, title,
description, description,
creator: '@OrganisationKA', creator: `@${process.env.NEXT_PUBLIC_TWITTER_USERNAME || 'OrganisationKA'}`,
images: { images: {
url: `${apiUrl}${kuvetiFormat?.url}`, url: `${apiUrl}${kuvetiFormat?.url}`,
alt: `Photo de ${anAwtis.alias}`, alt: `Photo de ${anAwtis.alias}`,
+14 -10
View File
@@ -10,17 +10,21 @@ import Pajinasyon from '../../components/awtis/pajinasyon'
import {jwennAwtisPajinasyon} from '../../lib/oki-api' import {jwennAwtisPajinasyon} from '../../lib/oki-api'
import Footer from '../../components/footer' import Footer from '../../components/footer'
const siteName = process.env.NEXT_PUBLIC_SITE_NAME || 'PAWÒL-NU. Paroles et traductions.'
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://pawol.nu'
const twitterHandle = `@${process.env.NEXT_PUBLIC_TWITTER_USERNAME || 'OrganisationKA'}`
export const metadata = { export const metadata = {
title: 'PAWÒL-NU | Artistes', title: `${siteName} | 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: 'PAWÒL-NU | Artistes', title: `${siteName} | 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: `${siteUrl}/awtis`,
siteName: 'PAWÒL-NU. Paroles et traductions.', siteName,
images: [ images: [
{ {
url: 'https://pawol.nu/logo-512x512.png', url: `${siteUrl}/logo-512x512.png`,
width: 512, width: 512,
height: 512 height: 512
} }
@@ -29,14 +33,14 @@ export const metadata = {
type: 'website' type: 'website'
}, },
twitter: { twitter: {
site: '@OrganisationKA', site: twitterHandle,
card: 'summary_large_image', card: 'summary_large_image',
title: 'PAWÒL-NU | Artistes', title: `${siteName} | 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: twitterHandle,
images: { images: {
url: 'https://pawol.nu/logo-512x512.png', url: `${siteUrl}/logo-512x512.png`,
alt: 'OKI Logo', alt: `${siteName} Logo`,
} }
} }
} }
+44 -29
View File
@@ -3,24 +3,33 @@ 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'
const siteName = process.env.NEXT_PUBLIC_SITE_NAME || 'PAWÒL-NU. Paroles et traductions.'
const siteDescription = process.env.NEXT_PUBLIC_SITE_DESCRIPTION || 'PAWÒL-NU a pour but de promouvoir le Medukam (ou Wanni Wannan) et les productions afro-diasporiques.'
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://pawol.nu'
const orgName = process.env.NEXT_PUBLIC_ORG_NAME || 'OKI'
const orgEmail = process.env.NEXT_PUBLIC_ORG_EMAIL || 'kontak@o-k-i.net'
const orgLocation = process.env.NEXT_PUBLIC_ORG_LOCATION || 'Guadeloupe'
const twitterHandle = `@${process.env.NEXT_PUBLIC_TWITTER_USERNAME || 'OrganisationKA'}`
const plausibleUrl = process.env.NEXT_PUBLIC_PLAUSIBLE_URL || null
export const metadata = { export const metadata = {
metadataBase: new URL('https://pawol.nu'), metadataBase: new URL(siteUrl),
manifest: '/manifest.json', manifest: '/manifest.webmanifest',
title: 'PAWÒL-NU. Paroles et traductions.', title: siteName,
description: 'PAWÒL-NU a pour but de promouvoir le Medukam (ou Wanni Wannan) et les productions afro-diasporiques.', description: siteDescription,
author: 'OKI', author: orgName,
category: 'music', category: 'music',
creator: 'OKI', creator: orgName,
publisher: 'OKI', publisher: orgName,
applicationName: 'PAWÒL-NU. Paroles et traductions.', applicationName: siteName,
openGraph: { openGraph: {
title: 'PAWÒL-NU. Paroles et traductions.', title: siteName,
description: 'PAWÒL-NU a pour but de promouvoir le Medukam (ou Wanni Wannan) et les productions afro-diasporiques.', description: siteDescription,
url: 'https://pawol.nu', url: siteUrl,
siteName: 'PAWÒL-NU. Paroles et traductions.', siteName,
images: [ images: [
{ {
url: 'https://pawol.nu/logo-512x512.png', url: `${siteUrl}/logo-512x512.png`,
width: 512, width: 512,
height: 512 height: 512
} }
@@ -29,14 +38,14 @@ export const metadata = {
type: 'website' type: 'website'
}, },
twitter: { twitter: {
site: '@OrganisationKA', site: twitterHandle,
card: 'summary_large_image', card: 'summary_large_image',
title: 'PAWÒL-NU. Paroles et traductions.', title: siteName,
description: 'PAWÒL-NU a pour but de promouvoir le Medukam (ou Wanni Wannan) et les productions afro-diasporiques.', description: siteDescription,
creator: '@OrganisationKA', creator: twitterHandle,
images: { images: {
url: 'https://pawol.nu/logo-512x512.png', url: `${siteUrl}/logo-512x512.png`,
alt: 'PAWÒL-NU Logo', alt: `${siteName} Logo`,
}, },
} }
} }
@@ -44,18 +53,16 @@ export const metadata = {
const jsonLd = { const jsonLd = {
'@context': 'https://schema.org', '@context': 'https://schema.org',
'@type': 'Organization', '@type': 'Organization',
url: 'https://pawol.nu', url: siteUrl,
email: 'kontak@o-k-i.net', email: orgEmail,
keywords: ['OKI', 'PAWÒL-NU', 'Paroles', 'Pawol', 'Medukam', 'Wanni Wannan'], keywords: [orgName, siteName, 'Paroles', 'Pawol'],
legalName: 'PAWÒL-NU', legalName: siteName,
location: 'Guadeloupe' location: orgLocation,
} }
export default async function RootLayout({children}) { export default async function RootLayout({children}) {
return ( const inner = (
<html lang='fr' suppressHydrationWarning> <>
<body>
<PlausibleProvider src='https://plausible.io/js/pa-3sQidCSfiSOXQUh-4La0T.js'>
<TopLoader color='#ffeb3b' /> <TopLoader color='#ffeb3b' />
<ThemeRegistry> <ThemeRegistry>
<Navigasyon /> <Navigasyon />
@@ -67,7 +74,15 @@ export default async function RootLayout({children}) {
dangerouslySetInnerHTML={{__html: JSON.stringify(jsonLd)}} dangerouslySetInnerHTML={{__html: JSON.stringify(jsonLd)}}
/> />
</section> </section>
</PlausibleProvider> </>
)
return (
<html lang='fr' suppressHydrationWarning>
<body>
{plausibleUrl
? <PlausibleProvider src={plausibleUrl}>{inner}</PlausibleProvider>
: inner}
</body> </body>
</html> </html>
) )
+25
View File
@@ -0,0 +1,25 @@
export default function manifest() {
return {
name: process.env.NEXT_PUBLIC_SITE_NAME || 'PAWÒL-NU',
short_name: process.env.NEXT_PUBLIC_SITE_SHORT_NAME || 'PAWÒL-NU',
description: process.env.NEXT_PUBLIC_SITE_DESCRIPTION || 'PAWÒL-NU a pour but de promouvoir le Medukam (ou Wanni Wannan) et les productions afro-diasporiques.',
scope: '/',
start_url: '/',
display: 'standalone',
background_color: '#303030',
theme_color: '#303030',
orientation: 'portrait-primary',
icons: [
{src: '/logo-72x72.png', type: 'image/png', sizes: '72x72'},
{src: '/logo-96x96.png', type: 'image/png', sizes: '96x96'},
{src: '/logo-128x128.png', type: 'image/png', sizes: '128x128'},
{src: '/logo-144x144.png', type: 'image/png', sizes: '144x144'},
{src: '/logo-152x152.png', type: 'image/png', sizes: '152x152'},
{src: '/logo-192x192.png', type: 'image/png', sizes: '192x192'},
{src: '/logo-256x256.png', type: 'image/png', sizes: '256x256'},
{src: '/logo-384x384.png', type: 'image/png', sizes: '384x384'},
{src: '/logo-512x512.png', type: 'image/png', sizes: '512x512'},
{src: '/maskable.png', type: 'image/png', sizes: '192x192', purpose: 'maskable'},
],
}
}
+5 -3
View File
@@ -1,7 +1,9 @@
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, jwennDenyeTeks} 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'
@@ -12,13 +14,13 @@ 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, denyeTeks] = await Promise.all([jwennStats(), jwennDenyeTeks()]) const [statistik, denyeTeks, anVedette] = await Promise.all([jwennStats(), jwennDenyeTeks(), jwennAnVedette()])
if (!statistik) { if (!statistik) {
notFound() notFound()
} }
return {statistik, dernierTeks: denyeTeks?.[0]} return {statistik, dernierTeks: anVedette ?? denyeTeks?.[0]}
} }
export default async function Page() { export default async function Page() {
+18 -3
View File
@@ -53,11 +53,11 @@ export async function generateMetadata(props) {
type: 'website' type: 'website'
}, },
twitter: { twitter: {
site: '@OrganisationKA', site: `@${process.env.NEXT_PUBLIC_TWITTER_USERNAME || 'OrganisationKA'}`,
card: 'summary_large_image', card: 'summary_large_image',
title, title,
description, description,
creator: '@OrganisationKA', creator: `@${process.env.NEXT_PUBLIC_TWITTER_USERNAME || 'OrganisationKA'}`,
images: { images: {
url: `${apiUrl}${kuvetiFormat?.url}`, url: `${apiUrl}${kuvetiFormat?.url}`,
alt: `Couverture ${title}`, alt: `Couverture ${title}`,
@@ -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 (
+14 -10
View File
@@ -4,17 +4,21 @@ import {jwennTeks} from '../../lib/oki-api'
import TeksDrawer from '../../components/teks/teks-drawer' import TeksDrawer from '../../components/teks/teks-drawer'
import Loading from './loading' import Loading from './loading'
const siteName = process.env.NEXT_PUBLIC_SITE_NAME || 'PAWÒL-NU. Paroles et traductions.'
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://pawol.nu'
const twitterHandle = `@${process.env.NEXT_PUBLIC_TWITTER_USERNAME || 'OrganisationKA'}`
export const metadata = { export const metadata = {
title: 'PAWÒL-NU. Paroles et traductions.', title: siteName,
description: 'Retrouvez les paroles et les traductions de vos chansons préférées.', description: 'Retrouvez les paroles et les traductions de vos chansons préférées.',
openGraph: { openGraph: {
title: 'PAWÒL-NU. Paroles et traductions.', title: siteName,
description: 'Retrouvez les paroles et les traductions de vos chansons préférées.', description: 'Retrouvez les paroles et les traductions de vos chansons préférées.',
url: 'https://pawol.nu/paroles', url: `${siteUrl}/paroles`,
siteName: 'PAWÒL-NU. Paroles et traductions.', siteName,
images: [ images: [
{ {
url: 'https://pawol.nu/logo-512x512.png', url: `${siteUrl}/logo-512x512.png`,
width: 512, width: 512,
height: 512 height: 512
} }
@@ -23,14 +27,14 @@ export const metadata = {
type: 'website' type: 'website'
}, },
twitter: { twitter: {
site: '@OrganisationKA', site: twitterHandle,
card: 'summary_large_image', card: 'summary_large_image',
title: 'PAWÒL-NU. Paroles et traductions.', title: siteName,
description: 'Retrouvez les paroles et les traductions de vos chansons préférées.', description: 'Retrouvez les paroles et les traductions de vos chansons préférées.',
creator: '@OrganisationKA', creator: twitterHandle,
images: { images: {
url: 'https://pawol.nu/logo-512x512.png', url: `${siteUrl}/logo-512x512.png`,
alt: 'OKI Logo', alt: `${siteName} Logo`,
}, },
} }
} }
+8 -6
View File
@@ -8,6 +8,8 @@ import Footer from '../../components/footer'
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 siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000' const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'
const siteName = process.env.NEXT_PUBLIC_SITE_NAME || 'PAWÒL-NU. Paroles et traductions.'
const twitterHandle = `@${process.env.NEXT_PUBLIC_TWITTER_USERNAME || 'OrganisationKA'}`
async function jwennDone() { async function jwennDone() {
const denyeTeks = await jwennDenyeTeks() const denyeTeks = await jwennDenyeTeks()
@@ -29,23 +31,23 @@ export async function generateMetadata() {
const description = `Derniers morceaux : ${songList}` const description = `Derniers morceaux : ${songList}`
return { return {
title: 'PAWÒL-NU | Derniers morceaux', title: `${siteName} | Derniers morceaux`,
description, description,
openGraph: { openGraph: {
title: 'PAWÒL-NU | Derniers morceaux', title: `${siteName} | Derniers morceaux`,
description, description,
url: `${siteUrl}/paroles`, url: `${siteUrl}/paroles`,
siteName: 'PAWÒL-NU. Paroles et traductions.', siteName,
images: [{url: imageUrl, width: imageWidth, height: imageHeight}], images: [{url: imageUrl, width: imageWidth, height: imageHeight}],
locale: 'fr_FR', locale: 'fr_FR',
type: 'website', type: 'website',
}, },
twitter: { twitter: {
site: '@OrganisationKA', site: twitterHandle,
card: 'summary_large_image', card: 'summary_large_image',
title: 'PAWÒL-NU | Derniers morceaux', title: `${siteName} | Derniers morceaux`,
description, description,
creator: '@OrganisationKA', creator: twitterHandle,
images: {url: imageUrl, alt: 'Couverture du dernier morceau publié'}, images: {url: imageUrl, alt: 'Couverture du dernier morceau publié'},
}, },
} }
+1 -1
View File
@@ -15,6 +15,6 @@ export default function robots() {
userAgent: '*', userAgent: '*',
allow: '/', allow: '/',
}, },
sitemap: 'https://pawol.nu/sitemap.xml', sitemap: `${process.env.NEXT_PUBLIC_SITE_URL || 'https://pawol.nu'}/sitemap.xml`,
} }
} }
+16 -10
View File
@@ -1,14 +1,20 @@
const siteName = process.env.NEXT_PUBLIC_SITE_NAME || 'PAWÒL-NU. Paroles et traductions.'
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://pawol.nu'
const orgFullName = process.env.NEXT_PUBLIC_ORG_FULL_NAME || process.env.NEXT_PUBLIC_ORG_NAME || 'ORGANISATION KA INTERNATIONALE'
const twitterHandle = `@${process.env.NEXT_PUBLIC_TWITTER_USERNAME || 'OrganisationKA'}`
const sipoteTitle = `${siteName} | Soutenir ${orgFullName} !`
export const metadata = { export const metadata = {
title: 'PAWÒL-NU | Soutenir ORGANISATION KA INTERNATIONALE !', title: sipoteTitle,
description: 'Vous pouvez nous soutenir via Liberapay ou PayPal', description: 'Vous pouvez nous soutenir via Liberapay ou PayPal',
openGraph: { openGraph: {
title: 'PAWÒL-NU | Soutenir ORGANISATION KA INTERNATIONALE !', title: sipoteTitle,
description: 'Vous pouvez nous soutenir via Liberapay ou PayPal.', description: 'Vous pouvez nous soutenir via Liberapay ou PayPal.',
url: 'https://pawol.nu/sipote', url: `${siteUrl}/sipote`,
siteName: 'PAWÒL-NU | Paroles et traductions.', siteName,
images: [ images: [
{ {
url: 'https://pawol.nu/sipote.png', url: `${siteUrl}/sipote.png`,
width: 500, width: 500,
height: 500 height: 500
} }
@@ -17,14 +23,14 @@ export const metadata = {
type: 'website' type: 'website'
}, },
twitter: { twitter: {
site: '@OrganisationKA', site: twitterHandle,
card: 'summary_large_image', card: 'summary_large_image',
title: 'PAWÒL-NU | Soutenir ORGANISATION KA INTERNATIONALE !', title: sipoteTitle,
description: 'Vous pouvez nous soutenir via Liberapay ou PayPal.', description: 'Vous pouvez nous soutenir via Liberapay ou PayPal.',
creator: '@OrganisationKA', creator: twitterHandle,
images: { images: {
url: 'https://pawol.nu/sipote.png', url: `${siteUrl}/sipote.png`,
alt: 'Sipòte OKI', alt: `Sipòte ${process.env.NEXT_PUBLIC_ORG_NAME || 'OKI'}`,
}, },
} }
} }
+19 -13
View File
@@ -4,23 +4,24 @@ import PropTypes from 'prop-types'
import Card from '@mui/material/Card' 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 Chip from '@mui/material/Chip' import Chip from '@mui/material/Chip'
import Image from 'next/image'
import Link from 'next/link' import Link from 'next/link'
import {getAlias} from '../../lib/utils/format' import {getAlias} from '../../lib/utils/format'
import {formatKuveti} from '../../lib/kuveti' import {formatKuveti} from '../../lib/kuveti'
const IMAGE_URL = process.env.NEXT_PUBLIC_API_URL_ROOT || 'http://localhost:1337' const IMAGE_URL = process.env.NEXT_PUBLIC_API_URL_ROOT || 'http://localhost:1337'
const noImageUrl = 'https://place-hold.it/600x600?text=Indisponible'
// 1×1 gris neutre — placeholder pendant le chargement
const BLUR_DATA_URL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNsYAAAAAYAAjCB0C8AAAAASUVORK5CYII='
export default function AnVedette({teks}) { export default function AnVedette({teks}) {
const {titre, artistes, annee, couverture, slug} = teks const {titre, artistes, annee, couverture, slug} = teks
const aliases = getAlias(artistes, teks.prioriteArtistes) const aliases = getAlias(artistes, teks.prioriteArtistes)
const fmt = formatKuveti(couverture) const fmt = formatKuveti(couverture, 'medium')
const imageUrl = fmt?.url ? `${IMAGE_URL}${fmt.url}` : noImageUrl
return ( return (
<Box sx={{mb: 4}}> <Box sx={{mb: 4}}>
@@ -32,17 +33,22 @@ export default function AnVedette({teks}) {
/> />
<Card sx={{borderRadius: 2, overflow: 'hidden'}}> <Card sx={{borderRadius: 2, overflow: 'hidden'}}>
<CardActionArea component={Link} href={`/paroles/${slug}`}> <CardActionArea component={Link} href={`/paroles/${slug}`}>
<CardMedia <Box sx={{position: 'relative', width: '100%', aspectRatio: {xs: '1 / 1', sm: '16 / 9'}, maxHeight: {sm: 420}}}>
component='img' {fmt?.url ? (
image={imageUrl} <Image
src={`${IMAGE_URL}${fmt.url}`}
alt={titre} alt={titre}
sx={{ fill
width: '100%', priority
aspectRatio: {xs: '1 / 1', sm: '16 / 9'}, placeholder='blur'
objectFit: 'cover', blurDataURL={BLUR_DATA_URL}
maxHeight: {sm: 420} sizes='(max-width: 600px) 100vw, 800px'
}} style={{objectFit: 'cover'}}
/> />
) : (
<Box sx={{width: '100%', height: '100%', bgcolor: 'grey.300'}} />
)}
</Box>
<CardContent> <CardContent>
<Typography variant='h5' component='h2' sx={{fontWeight: 'bold', mb: 0.5}}> <Typography variant='h5' component='h2' sx={{fontWeight: 'bold', mb: 0.5}}>
{titre} {titre}
+143 -42
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'
@@ -11,61 +12,119 @@ import Button from '@mui/material/Button'
import CardActionArea from '@mui/material/CardActionArea' import CardActionArea from '@mui/material/CardActionArea'
import Chip from '@mui/material/Chip' import Chip from '@mui/material/Chip'
import Container from '@mui/material/Container' import Container from '@mui/material/Container'
import Divider from '@mui/material/Divider'
import Grid from '@mui/material/Grid' 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 MusicNoteIcon from '@mui/icons-material/MusicNote'
import VerifiedIcon from '@mui/icons-material/Verified'
import {formatKuveti} from '../../lib/kuveti' import {formatKuveti} from '../../lib/kuveti'
import {StreamButton} from '../streaming-buttons'
import {SocialButton} from './social-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 EXCLUSIVE_LABEL = process.env.NEXT_PUBLIC_EXCLUSIVE_ARTIST_LABEL || 'OKI Exclusif'
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, isExclusiveArtist, titrePhare, rezoSosyal} = 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 = isExclusiveArtist && 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 maxWidth='sm'>
<Box sx={{marginTop: 8, marginBottom: 2}}> <Box sx={{mt: 2, mb: 1}}>
<Typography sx={{textAlign: 'center'}} variant='h6' component='h1'> <Link passHref href='/awtis'>
<Button size='small' variant='text' startIcon={<KeyboardBackspaceIcon />} sx={{color: 'text.secondary'}}>
Retour aux artistes
</Button>
</Link>
</Box>
{/* Héro */}
<Box sx={{display: 'flex', flexDirection: 'column', alignItems: 'center', pt: 2, pb: 3}}>
<Box sx={{
width: 150, height: 150,
borderRadius: '50%',
border: `3px solid ${green[500]}`,
overflow: 'hidden',
position: 'relative',
flexShrink: 0,
mb: 2,
}}>
{photoUrl ? (
<Image
src={photoUrl}
alt={`Photo ${alias}`}
width={150}
height={150}
placeholder='blur'
blurDataURL={BLUR_DATA_URL}
style={{objectFit: 'cover', width: '100%', height: '100%'}}
/>
) : (
<Box sx={{width: '100%', height: '100%', bgcolor: 'grey.300'}} />
)}
</Box>
<Typography variant='h5' component='h1' sx={{fontWeight: 700, mb: 1, textAlign: 'center'}}>
{alias} {alias}
</Typography> </Typography>
{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} />
))}
</Box> </Box>
<Box sx={{justifyContent: 'center', display: 'flex', marginBottom: 2}}> )}
<Avatar
src={photo?.url ? `${IMAGE_URL}${photo?.formats?.small?.url || photo?.formats?.thumbnail?.url || photo?.url}` : noImageUrl} {isExclusiveArtist && (
alt={`Photo ${alias}`} <Box sx={{display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 0.5}}>
sx={{width: 200, height: 200, border: `2px solid ${green[500]}`}} <Chip
icon={<VerifiedIcon sx={{fontSize: 16}} />}
label={`Artiste ${EXCLUSIVE_LABEL}`}
size='small'
sx={{bgcolor: '#FFD700', color: '#000', fontWeight: 700, '& .MuiChip-icon': {color: '#000'}}}
/> />
</Box> <Typography variant='caption' sx={{color: 'text.secondary', textAlign: 'center', maxWidth: 340}}>
<Grid sx={{alignItems:'center'}} container direction='column' spacing={3}> Certains morceaux sont publiés en exclusivité sur PAWÒL-NU, avant toute sortie sur les plateformes de streaming.
{biyo && (
<Grid size={{xs: 12, md: 6}}>
<Card sx={{minWidth: 300}}>
<CardActionArea onClick={handleClick}>
<CardContent>
<Typography gutterBottom variant='body1' component='h2'>
<strong>Biographie</strong>
</Typography> </Typography>
<Typography textalign='justify' variant='subtitle1' component='h3'> </Box>
)}
</Box>
<Grid container direction='column' spacing={2}>
{biyo && (
<Grid size={12}>
<Card>
<CardActionArea onClick={() => meteEsByografiOuve(true)}>
<CardContent>
<Typography gutterBottom variant='body2' sx={{fontWeight: 700, textTransform: 'uppercase', color: 'text.secondary', letterSpacing: 0.5}}>
Biographie
</Typography>
<Typography variant='body1'>
{biyo} {biyo}
</Typography> </Typography>
</CardContent> </CardContent>
@@ -73,23 +132,64 @@ export default function AwtisDetay({anAwtis}) {
</Card> </Card>
</Grid> </Grid>
)} )}
<Grid size={{xs: 12, md: 6}}>
<Box marginbottom={3}> {hasStreaming && (
<Grid size={12}>
<Card sx={{overflow: 'hidden'}}>
<Box sx={{px: 2, pt: 1.5, pb: 0.5, display: 'flex', alignItems: 'center', gap: 0.75}}>
<MusicNoteIcon sx={{fontSize: 14, color: 'text.secondary'}} />
<Typography variant='caption' sx={{fontWeight: 700, textTransform: 'uppercase', color: 'text.secondary', letterSpacing: 0.5}}>
Titre phare
</Typography>
</Box>
<Divider />
<Grid container>
{coverUrl && (
<Grid size={{xs: 12, sm: 4}}>
<Box sx={{position: 'relative', minHeight: 130, 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}}>
É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={12}>
{paroles.length > 1 ? ( {paroles.length > 1 ? (
<Accordion> <Accordion>
<AccordionSummary <AccordionSummary expandIcon={<ExpandMoreIcon />} aria-controls='panel-teks-content' id='panel-teks-header'>
expandIcon={<ExpandMoreIcon />} <Typography sx={{marginRight: 2}} variant='body1'><strong>Liste des paroles</strong></Typography>
aria-controls='panel-teks-content'
id='panel-teks-header'
>
<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 = couverture?.formats?.thumbnail || 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} />
@@ -98,28 +198,29 @@ export default function AwtisDetay({anAwtis}) {
})} })}
</AccordionDetails> </AccordionDetails>
</Accordion> </Accordion>
) : ( ) : paroles.length === 0 ? (
paroles.length === 0 ? ( <Typography textAlign='center' variant='body1' color='text.secondary'>
<Typography gutterBottom textAlign='center' variant='body1' component='h2'><strong>Aucune parole pour le moment</strong></Typography> Aucune parole pour le moment
</Typography>
) : ( ) : (
<Box> <Box>
<Typography gutterBottom textalign='center' variant='body1' component='h2'><strong>Parole</strong></Typography> <Typography gutterBottom variant='body1'><strong>Parole</strong></Typography>
<Paper sx={{height: '100%', paddingBlock: 2}}> <Paper sx={{paddingBlock: 2}}>
<MizikLyen anPawol={paroles[0]} kuveti={paroles[0].couverture?.formats?.thumbnail || formatKuveti(paroles[0].couverture)} /> <MizikLyen anPawol={paroles[0]} kuveti={paroles[0].couverture?.formats?.thumbnail || formatKuveti(paroles[0].couverture)} />
</Paper> </Paper>
</Box> </Box>
)
)} )}
</Box>
</Grid> </Grid>
</Grid> </Grid>
<Box sx={{textAlign: 'center', marginBlock: 3}} >
<Box sx={{textAlign: 'center', my: 4}}>
<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}
+19 -3
View File
@@ -10,14 +10,17 @@ 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'
const PREFIX = 'awtis-kat' const PREFIX = 'awtis-kat'
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3001' 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 IMAGE_URL = process.env.NEXT_PUBLIC_API_URL_ROOT || 'http://localhost:1337'
const EXCLUSIVE_LABEL = process.env.NEXT_PUBLIC_EXCLUSIVE_ARTIST_LABEL || 'OKI Exclusif'
const classes = { const classes = {
root: `${PREFIX}-root`, root: `${PREFIX}-root`,
@@ -55,13 +58,26 @@ 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, isExclusiveArtist} = 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', ...(isExclusiveArtist && {outline: '2px solid #FFD700'})}}>
<CardActionArea onClick={() => router.push(`${SITE_URL}/awtis/${slug}`)}> {isExclusiveArtist && (
<Chip
icon={<VerifiedIcon sx={{fontSize: 14}} />}
label={EXCLUSIVE_LABEL}
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'
+73
View File
@@ -0,0 +1,73 @@
'use client'
import Chip from '@mui/material/Chip'
import IconButton from '@mui/material/IconButton'
import Tooltip from '@mui/material/Tooltip'
import {
Mastodon, Peertube,
Instagram, Youtube, Tiktok,
Spotify, Deezer, Applemusic, Bandcamp, Soundcloud,
Facebook, Twitter,
} 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},
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},
}
export function SocialButton({rezo}) {
const config = SOCIAL_CONFIG[rezo.plateforme] ?? {label: rezo.plateforme, bg: '#555', color: '#fff', Icon: null}
const PlatformIcon = config.Icon
if (PlatformIcon) {
return (
<Tooltip title={config.label}>
<IconButton
component='a'
href={rezo.url}
target='_blank'
rel='noopener noreferrer'
size='small'
sx={{
bgcolor: config.bg,
width: 34,
height: 34,
'&:hover': {bgcolor: config.bg, opacity: 0.8},
}}
>
<PlatformIcon size={17} color={config.color} />
</IconButton>
</Tooltip>
)
}
return (
<Tooltip title={rezo.url}>
<Chip
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>
)
}
+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
} }
+17 -7
View File
@@ -2,7 +2,6 @@ import {useRef, useEffect} from 'react'
import {styled, useTheme} from '@mui/material/styles' import {styled, useTheme} from '@mui/material/styles'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import {Button, Dialog, DialogActions, DialogContent, DialogTitle, Typography} from '@mui/material' import {Button, Dialog, DialogActions, DialogContent, DialogTitle, Typography} from '@mui/material'
import {useRouter} from 'next/navigation'
import useMediaQuery from '@mui/material/useMediaQuery' import useMediaQuery from '@mui/material/useMediaQuery'
import Cgu from '.' import Cgu from '.'
@@ -23,17 +22,11 @@ const CGU_DOWNLOAD_LINK = process.env.NEXT_PUBLIC_CGU_DOWNLOAD_LINK
export default function CGUDialog({open, setOpen}) { export default function CGUDialog({open, setOpen}) {
const theme = useTheme() const theme = useTheme()
const fullScreen = useMediaQuery(theme.breakpoints.down('md')) const fullScreen = useMediaQuery(theme.breakpoints.down('md'))
const router = useRouter()
const handleClose = () => { const handleClose = () => {
setOpen(false) setOpen(false)
} }
const handleClick = event => {
event.preventDefault()
router.push(CGU_DOWNLOAD_LINK)
}
const descriptionElementRef = useRef(null) const descriptionElementRef = useRef(null)
useEffect(() => { useEffect(() => {
if (open) { if (open) {
@@ -64,7 +57,24 @@ export default function CGUDialog({open, setOpen}) {
id='scroll-dialog-description' id='scroll-dialog-description'
tabIndex={-1} tabIndex={-1}
> >
{CGU_DOWNLOAD_LINK ? (
<div style={{textAlign: 'center', padding: '2rem'}}>
<Typography paragraph>
Consultez nos CGU et notre politique de confidentialité :
</Typography>
<Button
variant='contained'
component='a'
href={CGU_DOWNLOAD_LINK}
target='_blank'
rel='noopener noreferrer'
>
Consulter les CGU
</Button>
</div>
) : (
<Cgu /> <Cgu />
)}
</Typography> </Typography>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
+4 -4
View File
@@ -71,7 +71,7 @@ export default function Cgu() {
Le responsable publication est une personne morale Le responsable publication est une personne morale
</Typography> </Typography>
<Typography variant='body1'> <Typography variant='body1'>
<strong>Réalisation</strong> : ORGANISATION KA INTERNATIONALE (OKI) <strong>Réalisation</strong> : Cédric Pronzola
</Typography> </Typography>
<List> <List>
<ListItem> <ListItem>
@@ -79,7 +79,7 @@ export default function Cgu() {
<ArrowRightAltIcon /> <ArrowRightAltIcon />
</ListItemIcon> </ListItemIcon>
<ListItemText> <ListItemText>
<strong>Courriel</strong> : <Link href='mailto:kontak@o-k-i.net'><strong>kontak@o-k-i.net</strong></Link> <strong>Courriel</strong> : <Link href='mailto:contact@cedric-pronzola.dev'><strong>contact@cedric-pronzola.dev</strong></Link>
</ListItemText> </ListItemText>
</ListItem> </ListItem>
<ListItem> <ListItem>
@@ -87,7 +87,7 @@ export default function Cgu() {
<ArrowRightAltIcon /> <ArrowRightAltIcon />
</ListItemIcon> </ListItemIcon>
<ListItemText> <ListItemText>
<strong>WEB</strong> : <Link target='_blank' rel='noreferrer' href='https://o-k-i.net'><strong>o-k-i.net</strong></Link> <strong>WEB</strong> : <Link target='_blank' rel='noreferrer' href='https://cedric-pronzola.dev'><strong>cedric-pronzola.dev</strong></Link>
</ListItemText> </ListItemText>
</ListItem> </ListItem>
<ListItem> <ListItem>
@@ -585,7 +585,7 @@ export default function Cgu() {
</Typography> </Typography>
<Typography gutterBottom variant='caption' > <Typography gutterBottom variant='caption' >
Dernières modifications le 12/05/2026 Dernières modifications le 26/06/2026
</Typography> </Typography>
</Root> </Root>
) )
+3 -4
View File
@@ -6,7 +6,7 @@ import Typography from '@mui/material/Typography'
import Box from '@mui/material/Box' import Box from '@mui/material/Box'
import Skeleton from '@mui/material/Skeleton' import Skeleton from '@mui/material/Skeleton'
import LinearProgress from '@mui/material/LinearProgress' import LinearProgress from '@mui/material/LinearProgress'
import {useTheme, useColorScheme, styled} from '@mui/material/styles' 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'
@@ -50,7 +50,6 @@ 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 [audioMeta, setAudioMeta] = useState(audioMetaCache)
const [downloading, setDownloading] = useState({}) const [downloading, setDownloading] = useState({})
@@ -211,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>
@@ -277,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>
+15 -12
View File
@@ -5,6 +5,9 @@ import Navigasyon from './navigasyon'
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000' const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'
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 siteName = process.env.NEXT_PUBLIC_SITE_NAME || 'PAWÒL-NU. Paroles et traductions.'
const orgName = process.env.NEXT_PUBLIC_ORG_NAME || 'OKI'
const twitterHandle = `@${process.env.NEXT_PUBLIC_TWITTER_USERNAME || 'OrganisationKA'}`
export default function HeadLayout({ export default function HeadLayout({
children, children,
@@ -20,38 +23,38 @@ export default function HeadLayout({
return ( return (
<div> <div>
<Head prefix='website: https://ogp.me/ns/website#'> <Head prefix='website: https://ogp.me/ns/website#'>
<title>{`${title ? `PAWÒL-NU | ${title}` : 'PAWÒL-NU. Paroles et traductions.'}`}</title> <title>{`${title ? `${siteName} | ${title}` : siteName}`}</title>
<link rel='canonical' href={`${slug ? `${siteUrl}/${slug}` : siteUrl}`} /> <link rel='canonical' href={`${slug ? `${siteUrl}/${slug}` : siteUrl}`} />
<link rel='manifest' href='/manifest.json' /> <link rel='manifest' href='/manifest.json' />
<link rel='icon' type='image/x-icon' sizes='32x32' href='/favicon.ico' /> <link rel='icon' type='image/x-icon' sizes='32x32' href='/favicon.ico' />
<link rel='apple-touch-icon' href='/favicon.ico' /> <link rel='apple-touch-icon' href='/favicon.ico' />
<meta name='monetization' content='$ilp.uphold.com/q7MFmYWNpwnr' /> <meta name='monetization' content='$ilp.uphold.com/q7MFmYWNpwnr' />
<meta name='application-name' content='PAWÒL-NU. Paroles et traductions.' /> <meta name='application-name' content={siteName} />
<meta name='twitter:card' content='summary_large_image' /> <meta name='twitter:card' content='summary_large_image' />
<meta name='twitter:url' content={`${slug ? `${siteUrl}/${slug}` : siteUrl}`} /> <meta name='twitter:url' content={`${slug ? `${siteUrl}/${slug}` : siteUrl}`} />
<meta name='twitter:title' content={`${title ? title : 'PAWÒL-NU. Paroles et traductions.'}`} /> <meta name='twitter:title' content={`${title ? title : siteName}`} />
<meta name='twitter:description' content={`${summary ? summary : 'PAWÒL-NU a pour but de promouvoir les langues et les productions afro-diasporiques.'}`} /> <meta name='twitter:description' content={`${summary ? summary : process.env.NEXT_PUBLIC_SITE_DESCRIPTION || 'PAWÒL-NU a pour but de promouvoir les langues et les productions afro-diasporiques.'}`} />
<meta name='twitter:image' content={`${imageUrl ? `${apiUrl}${imageUrl}` : `${siteUrl}/logo-192x192.png`}`} /> <meta name='twitter:image' content={`${imageUrl ? `${apiUrl}${imageUrl}` : `${siteUrl}/logo-192x192.png`}`} />
<meta name='twitter:creator' content='@OrganisationKA' /> <meta name='twitter:creator' content={twitterHandle} />
<meta name='twitter:site' content='@OrganisationKA' /> <meta name='twitter:site' content={twitterHandle} />
<meta name='theme-color' content='#303030' /> <meta name='theme-color' content='#303030' />
<meta name='apple-mobile-web-app-status-bar' content='#303030' /> <meta name='apple-mobile-web-app-status-bar' content='#303030' />
<meta charSet='utf-8' /> <meta charSet='utf-8' />
<meta name='description' content={`${summary ? summary : 'PAWÒL-NU a pour but de promouvoir les langues et les productions afro-diasporiques.'}`} /> <meta name='description' content={`${summary ? summary : process.env.NEXT_PUBLIC_SITE_DESCRIPTION || 'PAWÒL-NU a pour but de promouvoir les langues et les productions afro-diasporiques.'}`} />
<meta name='author' content='OKI' /> <meta name='author' content={orgName} />
<meta name='viewport' content='minimum-scale=1, initial-scale=1, width=device-width' /> <meta name='viewport' content='minimum-scale=1, initial-scale=1, width=device-width' />
<meta property='og:url' content={`${slug ? `${siteUrl}/${slug}` : siteUrl}`} /> <meta property='og:url' content={`${slug ? `${siteUrl}/${slug}` : siteUrl}`} />
<meta property='og:type' content='website' /> <meta property='og:type' content='website' />
<meta property='og:site_name' content={`${title ? title : 'PAWÒL-NU. Paroles et traductions.'}`} /> <meta property='og:site_name' content={`${title ? title : siteName}`} />
<meta property='og:title' content={`${title ? title : 'PAWÒL-NU. Paroles et traductions.'}`} /> <meta property='og:title' content={`${title ? title : siteName}`} />
<meta property='og:description' content={`${summary ? summary : 'PAWÒL-NU a pour but de promouvoir les langues et les productions afro-diasporiques.'}`} /> <meta property='og:description' content={`${summary ? summary : process.env.NEXT_PUBLIC_SITE_DESCRIPTION || 'PAWÒL-NU a pour but de promouvoir les langues et les productions afro-diasporiques.'}`} />
<meta property='og:locale' content='fr_FR' /> <meta property='og:locale' content='fr_FR' />
<meta property='og:image' content={`${imageUrl ? `${apiUrl}${imageUrl}` : `${siteUrl}/logo-512x512.png`}`} /> <meta property='og:image' content={`${imageUrl ? `${apiUrl}${imageUrl}` : `${siteUrl}/logo-512x512.png`}`} />
<meta property='og:image:secure_url' content={`${imageUrl ? `${apiUrl}${imageUrl}` : `${siteUrl}/logo-512x512.png`}`} /> <meta property='og:image:secure_url' content={`${imageUrl ? `${apiUrl}${imageUrl}` : `${siteUrl}/logo-512x512.png`}`} />
<meta property='og:image:type' content={imageMime ? imageMime : 'image/png'} /> <meta property='og:image:type' content={imageMime ? imageMime : 'image/png'} />
<meta property='og:image:width' content={imageWidth ? imageWidth : '512'} /> <meta property='og:image:width' content={imageWidth ? imageWidth : '512'} />
<meta property='og:image:height' content={imageHeight ? imageHeight : '512'} /> <meta property='og:image:height' content={imageHeight ? imageHeight : '512'} />
<meta property='og:image:alt' content={`${title && imageUrl ? title : 'PAWÒL-NU Logo'} | PAWÒL-NU. Paroles et traductions.`} /> <meta property='og:image:alt' content={`${title && imageUrl ? title : `${siteName} Logo`} | ${siteName}`} />
</Head> </Head>
<Navigasyon selectedTab={tab} /> <Navigasyon selectedTab={tab} />
{children} {children}
+1 -1
View File
@@ -70,7 +70,7 @@ export default function RezoDialog() {
onClose={handleClose} onClose={handleClose}
> >
<BootstrapDialogTitle id='rézo-dialog' onClose={handleClose}> <BootstrapDialogTitle id='rézo-dialog' onClose={handleClose}>
OKI sur le <strong>Fédiverse</strong> {process.env.NEXT_PUBLIC_ORG_NAME || 'OKI'} sur le <strong>Fédiverse</strong>
</BootstrapDialogTitle> </BootstrapDialogTitle>
<DialogContent> <DialogContent>
<Box> <Box>
+1 -1
View File
@@ -56,7 +56,7 @@ export default function Presantasyon() {
</ListItem> </ListItem>
</List> </List>
<Typography paragraph='true' variant='subtitle1' component='div'> <Typography paragraph='true' variant='subtitle1' component='div'>
Pour toute question, nhésitez pas à nous contacter par courriel <Link href='mailto:kontak@o-k-i.net'><strong>kontak@o-k-i.net</strong></Link>. Pour toute question, nhésitez pas à nous contacter par courriel <Link href={`mailto:${process.env.NEXT_PUBLIC_ORG_EMAIL || 'kontak@o-k-i.net'}`}><strong>{process.env.NEXT_PUBLIC_ORG_EMAIL || 'kontak@o-k-i.net'}</strong></Link>.
</Typography> </Typography>
<Typography paragraph='true' variant='subtitle1' component='div'> <Typography paragraph='true' variant='subtitle1' component='div'>
<strong>Merci par avance pour votre soutien 🥰</strong> <strong>Merci par avance pour votre soutien 🥰</strong>
+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,
}
+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}>
+14 -13
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,20 +22,15 @@ 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()
@@ -53,14 +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
src={`${IMAGE_URL}${couverture.formats?.thumbnail?.url || couverture.url}`}
alt={titre} alt={titre}
image={couverture?.url ? `${IMAGE_URL}${couverture.formats?.thumbnail?.url || couverture.url}` : noImageUrl} fill
title={titre} placeholder='blur'
loading='lazy' 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'>
+28 -32
View File
@@ -20,6 +20,7 @@ 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'
@@ -77,6 +78,8 @@ 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 = []
@@ -84,23 +87,23 @@ const langToArray = parole => {
const {francais, anglais, espagnol, allemand, italien} = parole.traductions const {francais, anglais, espagnol, allemand, italien} = parole.traductions
if (anglais) { if (anglais) {
langArray.push({title: 'Translation', flag: 'en', lang: anglais}) langArray.push({title: 'English', lang: anglais})
} }
if (francais) { if (francais) {
langArray.push({title: 'Traduction', flag: 'fr', lang: francais}) 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})
} }
} }
@@ -198,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 && (
@@ -215,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)}
+7 -4
View File
@@ -1,5 +1,8 @@
const {template} = require('lodash') const {template} = require('lodash')
const ORG_NAME = process.env.NEXT_PUBLIC_ORG_NAME || 'OKI'
const SITE_URL = process.env.SITE_URL || process.env.NEXT_PUBLIC_SITE_URL || 'https://pawol.nu'
const bodyTemplate = template(` const bodyTemplate = template(`
<!DOCTYPE html> <!DOCTYPE html>
<html lang="fr"> <html lang="fr">
@@ -65,18 +68,18 @@ const bodyTemplate = template(`
<body> <body>
<div> <div>
<img src="https://pawol.nu/logo-72x72.png" alt="Logo OKI"> <img src="${SITE_URL}/logo-72x72.png" alt="Logo ${ORG_NAME}">
</div> </div>
<div class="title"> <div class="title">
<h2 style="margin:0; mso-line-height-rule:exactly;">Merci !</h2><br> <h2 style="margin:0; mso-line-height-rule:exactly;">Merci !</h2><br>
<h3 style="margin:0; mso-line-height-rule:exactly;">Votre soutien à OKI est important et nous vous en sommes très reconnaissant ❤️</h3> <h3 style="margin:0; mso-line-height-rule:exactly;">Votre soutien à ${ORG_NAME} est important et nous vous en sommes très reconnaissant ❤️</h3>
</div> </div>
<div class="container"> <div class="container">
<section> <section>
<h4>Une aide indispensable</h4> <h4>Une aide indispensable</h4>
<p> <p>
En effet, ce sont les dons qui nous permettent de maintenir tous les services liés à OKI. En effet, ce sont les dons qui nous permettent de maintenir tous les services liés à ${ORG_NAME}.
</p> </p>
<p> <p>
Toute contribution, aussi modeste soit-elle, nous permet daugmenter nos capacités.<br /> Toute contribution, aussi modeste soit-elle, nous permet daugmenter nos capacités.<br />
@@ -94,7 +97,7 @@ const bodyTemplate = template(`
<p>Pour toute question, nhésitez pas à nous contacter en répondant à ce courriel.</p> <p>Pour toute question, nhésitez pas à nous contacter en répondant à ce courriel.</p>
<p>Merci infiniment pour votre soutien.</p> <p>Merci infiniment pour votre soutien.</p>
<span><i><span class="forceWhiteLink"><button><a href="https://pawol.nu" target="blank">PAWÒL-NU</a></button></span></i></span> <span><i><span class="forceWhiteLink"><button><a href="${SITE_URL}" target="blank">${ORG_NAME}</a></button></span></i></span>
</footer> </footer>
</html> </html>
`) `)
+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
} }
+37 -2
View File
@@ -59,6 +59,22 @@ export async function jwennTeksEpiSlug(slug) {
}, },
pawol: { pawol: {
populate: '*' populate: '*'
},
sourceOriginale: {
fields: ['titre', 'slug', 'annee'],
populate: {
artistes: {
fields: ['alias', 'slug']
}
}
},
remixes: {
fields: ['titre', 'slug', 'annee'],
populate: {
artistes: {
fields: ['alias', 'slug']
}
}
} }
}, },
filters: { filters: {
@@ -76,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', 'rezoSosyal'],
filters: { filters: {
slug: { slug: {
$eq: slug $eq: slug
@@ -95,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: ['isExclusiveArtist:desc', 'createdAt:desc'],
pagination: { pagination: {
start, start,
limit: AWTIS_POU_CHAK_PAJ limit: AWTIS_POU_CHAK_PAJ
@@ -213,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',
+17 -24
View File
@@ -5,6 +5,22 @@ const withPWA = require('next-pwa')({
skipWaiting: true skipWaiting: true
}) })
function buildRemotePatterns() {
const raw = process.env.NEXT_PUBLIC_DOMAINS_IMAGE || ''
const patterns = raw.split(' ').filter(Boolean).map(entry => {
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
return pattern
})
if (!raw.includes('localhost'))
patterns.push({protocol: 'http', hostname: 'localhost', port: '1337', pathname: '/uploads/**'})
if (!raw.includes('127.0.0.1'))
patterns.push({protocol: 'http', hostname: '127.0.0.1', port: '1337', pathname: '/uploads/**'})
return patterns
}
module.exports = (withPWA({ module.exports = (withPWA({
turbopack: {}, turbopack: {},
webpack: config => { webpack: config => {
@@ -16,29 +32,6 @@ module.exports = (withPWA({
return config return config
}, },
images: { images: {
remotePatterns: [ remotePatterns: buildRemotePatterns()
{
protocol: 'https',
hostname: 'api.pawol.nu',
pathname: '/uploads/**',
},
{
protocol: 'https',
hostname: 'pawol.nu',
},
{
protocol: 'http',
hostname: '127.0.0.1',
port: '1337',
pathname: '/uploads/**',
},
{
protocol: 'http',
hostname: 'localhost',
port: '1337',
pathname: '/uploads/**',
},
]
} }
})) }))
Binary file not shown.
+186
View File
@@ -0,0 +1,186 @@
# CGU et politique de confidentialité — PAWÒL-NU
## Définitions
**Prestations et Services** : [pawol.nu](https://pawol.nu) met à disposition :
**Contenu** : Ensemble des éléments constituants l'information présente sur le site, notamment textes images vidéos.
**Informations utilisateurs** : Ci après dénommé « Information(s) » qui correspondent à l'ensemble des données personnelles susceptibles d'être détenues par [pawol.nu](https://pawol.nu) pour la gestion de votre compte, et à des fins d'analyses et de statistiques.
**Utilisateur** : Internaute, utilisant le site susnommé.
**Utilisateur enregistré** : Internaute ayant compte utilisateur, utilisant le site susnommé.
**Informations personnelles** : « Les informations qui permettent, sous quelque forme que ce soit, directement ou non, l'identification des personnes physiques auxquelles elles s'appliquent » (article 4 de la loi n° 78-17 du 6 janvier 1978).
Les termes « données à caractère personnel », « personne concernée », « sous traitant » et « données sensibles » ont le sens défini par le Règlement Général sur la Protection des Données (RGPD : n° 2016-679)
## 1. Présentation du site internet
En vertu de l'article 6 de la loi n° 2004-575 du 21 juin 2004 pour la confiance dans l'économie numérique, il est précisé aux utilisateurs du site internet [pawol.nu](https://pawol.nu) l'identité des différents intervenants dans le cadre de sa réalisation et de son suivi :
**Propriétaire** : ORGANISATION KA INTERNATIONALE (Association loi 1901)
**Responsable publication** : ORGANISATION KA INTERNATIONALE [kontak@o-k-i.net](mailto:kontak@o-k-i.net)
Le responsable publication est une personne morale
**Réalisation** : Cédric Pronzola
- **Courriel** : [contact@cedric-pronzola.dev](mailto:contact@cedric-pronzola.dev)
- **WEB** : [cedric-pronzola.dev](https://cedric-pronzola.dev)
- **Git** : [LABOLA - OKI](https://labola.o-k-i.net/ORGANISATION-KA-INTERNATIONALE)
**Hébergeur** : OVH 2 rue Kellermann - 59100 Roubaix - France
**Délégué à la protection des données** : ORGANISATION KA INTERNATIONALE [kontak@o-k-i.net](mailto:kontak@o-k-i.net)
Ces mentions légales RGPD sont issues du [générateur gratuit de mentions légales pour un site internet (orson.io)](https://fr.orson.io/1371/generateur-mentions-legales).
## 2. Conditions générales d'utilisation du site et des services proposés
L'utilisation du site [pawol.nu](https://pawol.nu) implique l'acceptation pleine et entière des conditions générales d'utilisation ci-après décrites. Ces conditions d'utilisation sont susceptibles d'être modifiées ou complétées à tout moment, les utilisateurs du site [pawol.nu](https://pawol.nu) sont donc invités à les consulter de manière régulière.
Ce site internet est normalement accessible à tout moment aux utilisateurs. Une interruption pour raison de maintenance technique peut être toutefois décidée par [pawol.nu](https://pawol.nu), qui s'efforcera alors de communiquer préalablement aux utilisateurs les dates et heures de l'intervention. Le site web [pawol.nu](https://pawol.nu) est mis à jour régulièrement par **ORGANISATION KA INTERNATIONALE**. De la même façon, les mentions légales peuvent être modifiées à tout moment : elles s'imposent néanmoins à l'utilisateur qui est invité à s'y référer le plus souvent possible afin d'en prendre connaissance.
## 3. Description des services fournis
[pawol.nu](https://pawol.nu) s'efforce de fournir sur le site, des informations aussi précises que possible. Toutefois, il ne pourra être tenu responsable des oublis, des inexactitudes et des carences dans la mise à jour, qu'elles soient de son fait ou du fait des tiers partenaires qui lui fournissent ces informations.
Toutes les informations indiquées sur le site [pawol.nu](https://pawol.nu) sont données à titre indicatif, et sont susceptibles d'évoluer. Par ailleurs, les renseignements figurant sur le site [pawol.nu](https://pawol.nu) ne sont pas exhaustifs. Ils sont donnés sous réserve de modifications ayant été apportées depuis leur mise en ligne.
## 4. Limitations contractuelles sur les données techniques
Le site utilise la technologie JavaScript. Le site Internet ne pourra être tenu responsable de dommages matériels liés à l'utilisation du site. De plus, l'utilisateur du site s'engage à accéder au site en utilisant un matériel récent, ne contenant pas de virus et avec un navigateur de dernière génération mis-à-jour. Le site [pawol.nu](https://pawol.nu) est hébergé chez un prestataire sur le territoire de l'Union Européenne conformément aux dispositions du Règlement Général sur la Protection des Données (RGPD : n° 2016-679).
L'objectif est d'apporter une prestation qui assure le meilleur taux d'accessibilité. L'hébergeur assure la continuité de son service 24 Heures sur 24, tous les jours de l'année. Il se réserve néanmoins la possibilité d'interrompre le service d'hébergement pour les durées les plus courtes possibles notamment à des fins de maintenance, d'amélioration de ses infrastructures, de défaillance de ses infrastructures ou si les Prestations et Services génèrent un trafic réputé anormal.
[pawol.nu](https://pawol.nu) et l'hébergeur ne pourront être tenus responsables en cas de dysfonctionnement du réseau Internet, des lignes téléphoniques ou du matériel informatique et de téléphonie lié notamment à l'encombrement du réseau empêchant l'accès au serveur.
## 5. Limitations de responsabilité
[pawol.nu](https://pawol.nu) agit en tant qu'éditeur du site. [pawol.nu](https://pawol.nu) est responsable de la qualité et de la véracité du Contenu qu'il publie.
[pawol.nu](https://pawol.nu) ne pourra être tenu responsable des dommages directs et indirects causés au matériel de l'utilisateur, lors de l'accès au site internet [pawol.nu](https://pawol.nu), et résultant soit de l'utilisation d'un matériel ne répondant pas aux spécifications indiquées au point 4, soit de l'apparition d'un bug ou d'une incompatibilité.
## 6. Gestion des données personnelles
### 6.1 Responsables de la collecte des données personnelles
Pour les données personnelles collectées dans le cadre de la création du compte personnel de l'utilisateur et de sa navigation sur le site le responsable du traitement des données personnelles est : ORGANISATION KA INTERNATIONALE. [pawol.nu](https://pawol.nu).
En tant que responsable du traitement des données qu'il collecte, [pawol.nu](https://pawol.nu) s'engage à respecter le cadre des dispositions légales en vigueur. Chaque fois que [pawol.nu](https://pawol.nu) traite des Données Personnelles, [pawol.nu](https://pawol.nu) prend toutes les mesures raisonnables pour s'assurer de l'exactitude et de la pertinence des Données Personnelles au regard des finalités pour lesquelles [pawol.nu](https://pawol.nu) les traite.
### 6.2 Finalité des données collectées
[pawol.nu](https://pawol.nu) est susceptible de traiter tout ou partie des données :
- pour permettre la navigation sur le site et la gestion et la traçabilité des prestations et services commandés par l'utilisateur : données de connexion et d'utilisation du site
- pour prévenir et lutter contre la fraude informatique (spamming, hacking…) : matériel informatique utilisé pour la navigation, l'adresse IP, le mot de passe (hashé)
- pour améliorer la navigation sur le site : données de connexion et d'utilisation
### 6.3 Droit d'accès, de rectification et d'opposition
Conformément à la réglementation européenne en vigueur, les Utilisateurs de [pawol.nu](https://pawol.nu) disposent des droits suivants :
- droit d'accès (article 15 RGPD) et de rectification (article 16 RGPD), de mise à jour, de complétude des données des Utilisateurs droit de verrouillage ou d'effacement des données des Utilisateurs à caractère personnel (article 17 du RGPD), lorsqu'elles sont inexactes, incomplètes, équivoques, périmées, ou dont la collecte, l'utilisation, la communication ou la conservation est interdite
- droit de retirer à tout moment un consentement (article 13-2c RGPD)
- droit à la limitation du traitement des données des Utilisateurs (article 18 RGPD)
- droit à la portabilité des données que les Utilisateurs auront fournies, lorsque ces données font l'objet de traitements automatisés fondés sur leur consentement ou sur un contrat (article 20 RGPD)
- droit de définir le sort des données des Utilisateurs après leur mort et de choisir à qui [pawol.nu](https://pawol.nu) devra communiquer (ou non) ses données à un tiers qu'ils aura préalablement désigné
Dès que [pawol.nu](https://pawol.nu) a connaissance du décès d'un Utilisateur et à défaut d'instructions de sa part, [pawol.nu](https://pawol.nu) s'engage à détruire ses données, sauf si leur conservation s'avère nécessaire à des fins probatoires ou pour répondre à une obligation légale.
Si l'Utilisateur souhaite savoir comment [pawol.nu](https://pawol.nu) utilise ses Données Personnelles, demander à les rectifier ou s'oppose à leur traitement, l'Utilisateur peut contacter [pawol.nu](https://pawol.nu) par courriel à l'adresse suivante, [kontak@o-k-i.net](mailto:kontak@o-k-i.net) ou via XMPP à [oki@xmpp.cz](xmpp:oki@xmpp.cz).
Dans ce cas, l'Utilisateur doit indiquer les Données Personnelles qu'il souhaiterait que [pawol.nu](https://pawol.nu) corrige, mette à jour ou supprime, en s'identifiant précisément avec une copie d'une pièce d'identité (carte d'identité ou passeport).
Les demandes de suppression de Données Personnelles seront soumises aux obligations qui sont imposées à [pawol.nu](https://pawol.nu) par la loi, notamment en matière de conservation ou d'archivage des documents. Enfin, les Utilisateurs de [pawol.nu](https://pawol.nu) peuvent déposer une réclamation auprès des autorités de contrôle, et notamment de la CNIL (https://www.cnil.fr/fr/plaintes).
### 6.4 Non-communication des données personnelles
[pawol.nu](https://pawol.nu) s'interdit de traiter, héberger ou transférer les Informations collectées sur ses utilisateurs vers un pays situé en dehors de l'Union européenne ou reconnu comme « non adéquat » par la Commission européenne sans en informer préalablement l'utilisateur. Pour autant, [pawol.nu](https://pawol.nu) reste libre du choix de ses sous-traitants techniques et commerciaux à la condition qu'il présentent les garanties suffisantes au regard des exigences du Règlement Général sur la Protection des Données (RGPD : n° 2016-679).
[pawol.nu](https://pawol.nu) s'engage à prendre toutes les précautions nécessaires afin de préserver la sécurité des Informations et notamment qu'elles ne soient pas communiquées à des personnes non autorisées. Cependant, si un incident impactant l'intégrité ou la confidentialité des Informations de l'utilisateur est portée à la connaissance de [pawol.nu](https://pawol.nu), celle-ci devra dans les meilleurs délais informer l'utilisateur et lui communiquer les mesures de corrections prises. Par ailleurs [pawol.nu](https://pawol.nu) ne collecte aucune « données sensibles ».
### 6.5 Inscription et connexion
Les utilisateurs peuvent s'inscrire sur [pawol.nu](https://pawol.nu) en renseignant leur adresse e-mail, un nom d'utilisateur ainsi qu'un mot de passe. À la suite de l'inscription, un lien de vérification est envoyé à l'adresse utilisée. Ce lien permet d'activer le compte. Sans cette activation, l'utilisateur n'aura pas la possibilité de se connecter au site. Si un utilisateur n'a pas reçu le lien d'activation, il a la possibilité de demander son renvoi. L'utilisateur ayant oublié son mot de passe peut faire la demande de renouvellement de celui-ci. Avant toute inscription, il est important de noté que le nom d'utilisateur est public et donc accessible à tous. En effet, le site fait appel à une API qui permet la consultation de plusieurs éléments, dont l'identifiant de l'utilisateur ainsi que le nom qu'il aura choisi au moment de l'inscription. Cependant, l'adresse e-mail ne figure pas dans les informations accessibles. Seul [pawol.nu](https://pawol.nu) en à connaissance. Les utilisateurs peuvent également se connecter en utilisant un compte Twitter. Dans ce cas, c'est l'identifiant de cette plateforme qui sera affiché en cas de contribution. Il est également possible de se connecter grâce à un compte Google. En acceptant d'utiliser la fonctionnalité de connexion via Google ou Twitter, vous autorisez [pawol.nu](https://pawol.nu) à récupérer les informations liées à votre profil, que vous avez transmises à ces plateformes lors de votre inscription. Un compte GitHub permet désormais de se connecter au site. Nous utilisons uniquement le nom d'utilisateur, ainsi que l'adresse e-mail.
### 6.6 Proposition de paroles
Les utilisateurs connectés ont la possibilité de proposer des paroles depuis un espace dédié. Il se trouve à l'adresse [pawol.nu/pwopose](https://pawol.nu/pwopose). Les textes soumis ne sont pas directement publiés sur le site. Ils doivent être approuvé par l'équipe de vérification. Après approbation, il apparaît sur le site avec le nom de l'utilisateur l'ayant proposé.
## 7. Notification d'incident
Quels que soient les efforts fournis, aucune méthode de transmission sur Internet et aucune méthode de stockage électronique n'est complètement sûre. Nous ne pouvons en conséquence pas garantir une sécurité absolue. Si nous prenions connaissance d'une brèche de la sécurité, nous avertirions les utilisateurs concernés afin qu'ils puissent prendre les mesures appropriées. Nos procédures de notification d'incident tiennent compte de nos obligations légales, qu'elles se situent au niveau national ou européen. Nous nous engageons à informer pleinement nos utilisateurs de toutes les questions relevant de la sécurité de leur compte et à leur fournir toutes les informations nécessaires pour les aider à respecter leurs propres obligations réglementaires en matière de reporting.
Aucune information personnelle de l'utilisateur du site [pawol.nu](https://pawol.nu) n'est publiée à l'insu de l'utilisateur, échangée, transférée, cédée ou vendue sur un support quelconque à des tiers. Seule l'hypothèse du rachat de [pawol.nu](https://pawol.nu) et de ses droits permettrait la transmission des dites informations à l'éventuel acquéreur qui serait à son tour tenu de la même obligation de conservation et de modification des données vis à vis de l'utilisateur du site [pawol.nu](https://pawol.nu).
## Sécurité
Pour assurer la sécurité et la confidentialité des Données Personnelles et des Données Personnelles de Santé, [pawol.nu](https://pawol.nu) utilise des réseaux protégés par des dispositifs standards tels que par pare-feu, la pseudonymisation, l'encryption et mot de passe.
Lors du traitement des Données Personnelles, [pawol.nu](https://pawol.nu) prend toutes les mesures raisonnables visant à les protéger contre toute perte, utilisation détournée, accès non autorisé, divulgation, altération ou destruction.
## 8. Liens hypertextes « cookies » et balises ("tags") internet
Le site [pawol.nu](https://pawol.nu) contient un certain nombre de liens hypertextes vers d'autres sites, mis en place avec l'autorisation de [pawol.nu](https://pawol.nu). Cependant, [pawol.nu](https://pawol.nu) n'a pas la possibilité de vérifier le contenu des sites ainsi visités, et n'assumera en conséquence aucune responsabilité de ce fait. Sauf si vous décidez de désactiver les cookies, vous acceptez que le site puisse les utiliser. Vous pouvez à tout moment désactiver ces cookies et ce gratuitement à partir des possibilités de désactivation qui vous sont offertes et rappelées ci-après, sachant que cela peut réduire ou empêcher l'accessibilité à tout ou partie des Services proposés par le site.
### 8.1 « COOKIES »
Un « cookie » est un petit fichier d'information envoyé sur le navigateur de l'Utilisateur et enregistré au sein du terminal de l'Utilisateur (ex : ordinateur, smartphone), (ci-après « Cookies »). Ce fichier comprend des informations telles que le nom de domaine de l'Utilisateur, le fournisseur d'accès Internet de l'Utilisateur, le système d'exploitation de l'Utilisateur, ainsi que la date et l'heure d'accès. Les Cookies ne risquent en aucun cas d'endommager le terminal de l'Utilisateur.
[pawol.nu](https://pawol.nu) est susceptible de traiter les informations de l'Utilisateur concernant sa visite du Site, telles que les pages consultées, les recherches effectuées. Ces informations permettent à [pawol.nu](https://pawol.nu) d'améliorer le contenu du Site, de la navigation de l'Utilisateur.
Les Cookies facilitant la navigation et/ou la fourniture des services proposés par le Site, l'Utilisateur peut configurer son navigateur pour qu'il lui permette de décider s'il souhaite ou non les accepter de manière à ce que des Cookies soient enregistrés dans le terminal ou, au contraire, qu'ils soient rejetés, soit systématiquement, soit selon leur émetteur. L'Utilisateur peut également configurer son logiciel de navigation de manière à ce que l'acceptation ou le refus des Cookies lui soient proposés ponctuellement, avant qu'un Cookie soit susceptible d'être enregistré dans son terminal. [pawol.nu](https://pawol.nu) informe l'Utilisateur que, dans ce cas, il se peut que les fonctionnalités de son logiciel de navigation ne soient pas toutes disponibles.
Si l'Utilisateur refuse l'enregistrement de Cookies dans son terminal ou son navigateur, ou si l'Utilisateur supprime ceux qui y sont enregistrés, l'Utilisateur est informé que sa navigation et son expérience sur le Site peuvent être limitées. Cela pourrait également être le cas lorsque [pawol.nu](https://pawol.nu) ne peut pas reconnaître, à des fins de compatibilité technique, le type de navigateur utilisé par le terminal, les paramètres de langue et d'affichage ou le pays depuis lequel le terminal semble connecté à Internet.
Le cas échéant, [pawol.nu](https://pawol.nu) décline toute responsabilité pour les conséquences liées au fonctionnement dégradé du Site et des services éventuellement proposés par [pawol.nu](https://pawol.nu), résultant du refus de Cookies par l'Utilisateur de l'impossibilité pour [pawol.nu](https://pawol.nu) d'enregistrer ou de consulter les Cookies nécessaires à leur fonctionnement du fait du choix de l'Utilisateur.
Enfin, en cliquant sur l'icône dédiée au réseau social Twitter, figurant sur le Site de [pawol.nu](https://pawol.nu) ou dans son application mobile et si l'Utilisateur a accepté le dépôt de cookies en poursuivant sa navigation sur le Site Internet ou l'application mobile de [pawol.nu](https://pawol.nu), Twitter, peut également déposer des cookies sur vos terminaux (ordinateur, tablette, téléphone portable).
Ces types de cookies ne sont déposés sur vos terminaux qu'à condition que vous y consentiez, en continuant votre navigation sur le Site Internet ou l'application mobile de [pawol.nu](https://pawol.nu). À tout moment, l'Utilisateur peut néanmoins revenir sur son consentement à ce que [pawol.nu](https://pawol.nu) dépose ce type de cookies.
## 9. Les dons
Le site dispose d'une page permettant de faire des dons à « ORGANISATION KA INTERNATIONALE » de manière sécurisée. Pour se faire, il faut se rendre sur la page « [soutyen](https://pawol.nu/soutyen) ». L'utilisateur a 2 onglets disponibles :
- LIBERAPAY / PAYPAL
- CARTE BANCAIRE
### 9.1 LIBERAPAY / PAYPAL
La plateforme [Liberapay](https://liberapay.com/OKi/donate) est mise à disposition. Elle permet non seulement de personnaliser le montant, mais aussi la récurrence des dons. C'est à dire qu'avec cet outil, l'utilisateur peut effectuer des dons hebdomadaires, mensuels ou annuels.
L'utilisateur peut faire un don en passant par la plateforme [PayPal](https://www.paypal.com/donate/?hosted_button_id=5Q3KPR79CAZVW). En cliquant sur le bouton prévu à cet effet, il est redirigé vers la page de don associée à ORGANISATION KA INTERNATIONALE. Il peut ensuite choisir un montant et valider le don en utilisant son compte PayPal ou une carte bancaire s'il n'est pas enregistré.
### 9.2 CARTE BANCAIRE
En cliquant sur le bouton « CARTE BANCAIRE », l'utilisateur se voit proposer plusieurs choix. Parmi ces derniers, deux options sont possibles :
- **Ponctuel** (1€, 5€, 10€, 20€, 50€, Montant personnalisé)
- **Mensuel** (1€ / mois, 5€ / mois, 10€ / mois, 20€ / mois, 50€ / mois)
Les dons peuvent donc être fait une seule fois ou de manière récurrente. Quelque soit le choix, l'utilisateur est redirigé vers le sous-domaine don.o-k-i.net suivi de l'ID du montant choisit. Cette URL est gérée par la plateforme de paiement sécurisée [Stripe](https://stripe.com/). Il est important de noter qu'**aucune information de paiement n'est stockée sur notre site internet**.
## 10. Propriété intellectuelle et licence
Le site internet [pawol.nu](https://pawol.nu) et les éléments qui y sont accessibles (textes, images, graphismes, logos, vidéos, icônes, sons, etc.) sont, sauf mention contraire, mis à disposition sous la licence [GNU Affero General Public License Version 3 (AGPL-3.0)](https://www.gnu.org/licenses/agpl-3.0.html). Cette licence garantit aux utilisateurs les libertés suivantes :
- Liberté d'exécuter le programme pour tous les usages,
- Liberté d'étudier le fonctionnement du programme et de l'adapter à vos besoins,
- Liberté de redistribuer des copies du programme,
- Liberté d'améliorer le programme et de publier vos modifications.
Conformément aux exigences de la licence AGPL-3.0, si vous redistribuez ou modifiez des éléments du site, vous devez également fournir le code source complet, incluant vos modifications, sous la même licence. Vous pouvez consulter les termes détaillés de la licence AGPL-3.0 sur le site officiel de la [Free Software Foundation](https://www.gnu.org/licenses/agpl-3.0.html).
## 11. Droit applicable et attribution de juridiction
Tout litige en relation avec l'utilisation du site [pawol.nu](https://pawol.nu) est soumis au droit français. En dehors des cas où la loi ne le permet pas, il est fait attribution exclusive de juridiction aux tribunaux compétents.
---
*Dernières modifications le 26/06/2026*
-64
View File
@@ -1,64 +0,0 @@
{
"name": "PAWÒL-NU",
"short_name": "PAWÒL-NU",
"description": "PAWÒL-NU a pour but de promouvoir le Medukam (ou Wanni Wannan) et les productions afro-diasporiques.",
"scope": "/",
"start_url": "/",
"display": "standalone",
"background_color": "#303030",
"theme_color": "#303030",
"orientation": "portrait-primary",
"icons": [
{
"src": "/logo-72x72.png",
"type": "image/png",
"sizes": "72x72"
},
{
"src": "/logo-96x96.png",
"type": "image/png",
"sizes": "96x96"
},
{
"src": "/logo-128x128.png",
"type": "image/png",
"sizes": "128x128"
},
{
"src": "/logo-144x144.png",
"type": "image/png",
"sizes": "144x144"
},
{
"src": "/logo-152x152.png",
"type": "image/png",
"sizes": "152x152"
},
{
"src": "/logo-192x192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "/logo-256x256.png",
"type": "image/png",
"sizes": "256x256"
},
{
"src": "/logo-384x384.png",
"type": "image/png",
"sizes": "384x384"
},
{
"src": "/logo-512x512.png",
"type": "image/png",
"sizes": "512x512"
},
{
"src": "maskable_oki.png",
"type": "image/png",
"sizes": "192x192",
"purpose": "maskable"
}
]
}

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env bash
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
CGU_DIR="$SCRIPT_DIR/../public/cgu"
MD_FILE="$CGU_DIR/cgu.md"
DATE=$(date +%d-%m-%Y)
PDF_FILE="$CGU_DIR/cgu-confidentialite-oki-$DATE.pdf"
TMP_HTML=$(mktemp /tmp/cgu-XXXXXX.html)
trap 'rm -f "$TMP_HTML"' EXIT
cat > "$TMP_HTML" <<'HTML_HEAD'
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>CGU et politique de confidentialité — PAWÒL-NU</title>
<style>
body { font-family: "Segoe UI", Arial, sans-serif; font-size: 13px; color: #222; max-width: 800px; margin: 40px auto; padding: 0 30px; line-height: 1.6; text-align: justify; }
h1 { font-size: 20px; margin-bottom: 4px; }
h2 { font-size: 16px; margin-top: 28px; margin-bottom: 6px; border-bottom: 1px solid #ccc; padding-bottom: 4px; }
h3 { font-size: 14px; margin-top: 18px; margin-bottom: 4px; }
p { margin: 8px 0; }
ul { margin: 6px 0 6px 20px; padding: 0; }
li { margin: 4px 0; }
a { color: #1a73e8; text-decoration: none; }
hr { border: none; border-top: 1px solid #ddd; margin: 32px 0; }
em { color: #888; font-size: 11px; }
</style>
</head>
<body>
HTML_HEAD
node - "$MD_FILE" >> "$TMP_HTML" <<'NODE_EOF'
const fs = require('fs')
const md = fs.readFileSync(process.argv[2], 'utf8')
let html = md
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/^# (.+)$/gm, '<h1>$1</h1>')
.replace(/^## (.+)$/gm, '<h2>$1</h2>')
.replace(/^### (.+)$/gm, '<h3>$1</h3>')
.replace(/^\*\*\*(.+)\*\*\*$/gm, '<hr>')
.replace(/^---$/gm, '<hr>')
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.+?)\*/g, '<em>$1</em>')
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>')
.replace(/^- (.+)$/gm, '<li>$1</li>')
.replace(/(<li>.*<\/li>\n?)+/gs, m => `<ul>${m}</ul>`)
.replace(/\n\n/g, '</p><p>')
.replace(/^(?!<[hup]|<\/[hup]|<li|<\/li|<ul|<\/ul|<hr)(.+)$/gm, '$1')
process.stdout.write(`<p>${html}</p>`)
NODE_EOF
echo '</body></html>' >> "$TMP_HTML"
chromium --headless --no-sandbox --no-pdf-header-footer \
--print-to-pdf="$PDF_FILE" "$TMP_HTML" 2>/dev/null
echo "PDF généré : $PDF_FILE"
echo "Pense à mettre à jour NEXT_PUBLIC_CGU_DOWNLOAD_LINK=/cgu/$(basename "$PDF_FILE")"