Compare commits
47
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eae8bb03a9
|
||
|
|
454eefbe58
|
||
|
|
b193914c57
|
||
|
|
d700ee6def
|
||
|
|
2132df3e0c
|
||
|
|
c7e3faf10a
|
||
|
|
4c662e89b5
|
||
|
|
2bec90210a
|
||
|
|
44e1a426b0
|
||
|
|
d2971870c1
|
||
|
|
1d8edbbcd5
|
||
|
|
a669abbfc2
|
||
|
|
69715a1a22
|
||
|
|
7b9968cb82
|
||
|
|
77f8cea399
|
||
|
|
c407a1fad9
|
||
|
|
c7104c6a0b
|
||
|
|
c4dca87b61
|
||
|
|
24f4f65853
|
||
|
|
3f30f699f7
|
||
|
|
399796bb73
|
||
|
|
b67bdf0632
|
||
|
|
9f9b408f1f
|
||
|
|
15367f388e
|
||
|
|
7234c1408f
|
||
|
|
e4d31b0243
|
||
|
|
96b7a601c8
|
||
|
|
a56b355493
|
||
|
|
55967bfc3c
|
||
|
|
daa3510d73
|
||
|
|
cad6e39f53
|
||
|
|
25066e97ce
|
||
|
|
85e9142a32
|
||
|
|
054a45246e
|
||
|
|
9752e4bcdd
|
||
|
|
719522f665 | ||
|
|
1c86d0b962
|
||
|
|
96dafb7b54
|
||
|
|
d64aa863dc
|
||
|
|
7b0c01ce06
|
||
|
|
86998dca42
|
||
|
|
e3366dd821
|
||
|
|
475cbe9028
|
||
|
|
288d771075 | ||
|
|
cd5ebb8058
|
||
|
|
aa3032d25e
|
||
|
|
cd24d90b67
|
+3
-8
@@ -4,7 +4,6 @@ NEXT_PUBLIC_API_URL=$API_URL
|
|||||||
NEXT_PUBLIC_API_URL_ROOT=http://localhost:1337
|
NEXT_PUBLIC_API_URL_ROOT=http://localhost:1337
|
||||||
SITE_URL=http://localhost:3001
|
SITE_URL=http://localhost:3001
|
||||||
NEXT_PUBLIC_READ_TOKEN=
|
NEXT_PUBLIC_READ_TOKEN=
|
||||||
NEXT_PUBLIC_ADMIN_JWT_SECRET=
|
|
||||||
|
|
||||||
# IDENTITÉ DU SITE (branding)
|
# IDENTITÉ DU SITE (branding)
|
||||||
NEXT_PUBLIC_SITE_NAME=PAWÒL-NU. Paroles et traductions.
|
NEXT_PUBLIC_SITE_NAME=PAWÒL-NU. Paroles et traductions.
|
||||||
@@ -18,8 +17,8 @@ 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
|
MIZIK_API_USER=user
|
||||||
NEXT_PUBLIC_MIZIK_API_PASSWORD=password
|
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
|
||||||
@@ -42,7 +41,7 @@ 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)
|
# Debug (mettre YES pour afficher les emails dans les logs en développement)
|
||||||
SHOW_EMAILS=
|
SHOW_EMAILS=
|
||||||
@@ -77,7 +76,6 @@ NEXT_PUBLIC_LIBERAPAY_DONATE=
|
|||||||
NEXT_PUBLIC_PALE_USERNAME=
|
NEXT_PUBLIC_PALE_USERNAME=
|
||||||
NEXT_PUBLIC_GADE_USERNAME=
|
NEXT_PUBLIC_GADE_USERNAME=
|
||||||
NEXT_PUBLIC_YOUTUBE_USERNAME=
|
NEXT_PUBLIC_YOUTUBE_USERNAME=
|
||||||
NEXT_PUBLIC_TELEGRAM_GROUP=
|
|
||||||
NEXT_PUBLIC_XMPP=
|
NEXT_PUBLIC_XMPP=
|
||||||
NEXT_PUBLIC_GIT=
|
NEXT_PUBLIC_GIT=
|
||||||
NEXT_PUBLIC_CODEBERG=
|
NEXT_PUBLIC_CODEBERG=
|
||||||
@@ -86,9 +84,6 @@ 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"
|
||||||
|
|
||||||
# JWT SECRET
|
|
||||||
NEXT_PUBLIC_JWT_SECRET=
|
|
||||||
|
|
||||||
# STRIPE
|
# STRIPE
|
||||||
|
|
||||||
NEXT_PUBLIC_STRIPE_PUBLIC_KEY=
|
NEXT_PUBLIC_STRIPE_PUBLIC_KEY=
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ jobs:
|
|||||||
- name: Vérifier les dépendances
|
- name: Vérifier les dépendances
|
||||||
run: yarn install --frozen-lockfile
|
run: yarn install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Lancer le lint
|
||||||
|
run: yarn lint
|
||||||
|
|
||||||
|
- name: Lancer les tests
|
||||||
|
run: yarn test
|
||||||
|
|
||||||
deploy-beta:
|
deploy-beta:
|
||||||
needs: check
|
needs: check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -22,6 +20,12 @@ jobs:
|
|||||||
- name: Vérifier les dépendances
|
- name: Vérifier les dépendances
|
||||||
run: yarn install --frozen-lockfile
|
run: yarn install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Lancer le lint
|
||||||
|
run: yarn lint
|
||||||
|
|
||||||
|
- name: Lancer les tests
|
||||||
|
run: yarn test
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
needs: check
|
needs: check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -36,7 +40,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
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ jobs:
|
|||||||
- name: Vérifier les dépendances
|
- name: Vérifier les dépendances
|
||||||
run: yarn install --frozen-lockfile
|
run: yarn install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Lancer le lint
|
||||||
|
run: yarn lint
|
||||||
|
|
||||||
|
- name: Lancer les tests
|
||||||
|
run: yarn test
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
needs: check
|
needs: check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import {describe, it, expect, afterEach} from 'vitest'
|
||||||
|
import robots from '../robots'
|
||||||
|
|
||||||
|
describe('robots', () => {
|
||||||
|
const originalNodeEnv = process.env.NODE_ENV
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
process.env.NODE_ENV = originalNodeEnv
|
||||||
|
})
|
||||||
|
|
||||||
|
it('autorise l\'indexation en production', () => {
|
||||||
|
process.env.NODE_ENV = 'production'
|
||||||
|
|
||||||
|
expect(robots().rules).toEqual({userAgent: '*', allow: '/'})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('bloque l\'indexation hors production', () => {
|
||||||
|
process.env.NODE_ENV = 'development'
|
||||||
|
|
||||||
|
expect(robots().rules).toEqual({userAgent: '*', disallow: '/'})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import {describe, it, expect, vi, afterEach} from 'vitest'
|
||||||
|
import {GET} from '../route'
|
||||||
|
|
||||||
|
function fakeUpstreamResponse({status = 200, headers = {}, body = 'audio-bytes'} = {}) {
|
||||||
|
return {
|
||||||
|
status,
|
||||||
|
body,
|
||||||
|
headers: {
|
||||||
|
get: key => headers[key.toLowerCase()] ?? null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('GET /api/mizik-stream/[id]', () => {
|
||||||
|
const originalFetch = global.fetch
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
global.fetch = originalFetch
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('n\'expose pas les identifiants au client, uniquement l\'id dans l\'URL amont', async () => {
|
||||||
|
global.fetch = vi.fn(async () => fakeUpstreamResponse())
|
||||||
|
|
||||||
|
const request = new Request('http://localhost/api/mizik-stream/42')
|
||||||
|
await GET(request, {params: Promise.resolve({id: '42'})})
|
||||||
|
|
||||||
|
const [calledUrl] = global.fetch.mock.calls[0]
|
||||||
|
expect(calledUrl).toContain('id=42')
|
||||||
|
expect(calledUrl).toMatch(/[?&]u=/)
|
||||||
|
expect(calledUrl).toMatch(/[?&]p=/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('transmet l\'en-tête Range pour permettre le seek audio', async () => {
|
||||||
|
global.fetch = vi.fn(async () => fakeUpstreamResponse({status: 206, headers: {'content-range': 'bytes 0-99/200'}}))
|
||||||
|
|
||||||
|
const request = new Request('http://localhost/api/mizik-stream/42', {headers: {range: 'bytes=0-99'}})
|
||||||
|
const response = await GET(request, {params: Promise.resolve({id: '42'})})
|
||||||
|
|
||||||
|
const [, options] = global.fetch.mock.calls[0]
|
||||||
|
expect(options.headers.range).toBe('bytes=0-99')
|
||||||
|
expect(response.status).toBe(206)
|
||||||
|
expect(response.headers.get('content-range')).toBe('bytes 0-99/200')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renvoie le flux et le statut renvoyés par le serveur Funkwhale', async () => {
|
||||||
|
global.fetch = vi.fn(async () => fakeUpstreamResponse({status: 200, headers: {'content-type': 'audio/mpeg'}}))
|
||||||
|
|
||||||
|
const request = new Request('http://localhost/api/mizik-stream/7')
|
||||||
|
const response = await GET(request, {params: Promise.resolve({id: '7'})})
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(response.headers.get('content-type')).toBe('audio/mpeg')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
const MIZIK_URL = process.env.NEXT_PUBLIC_OKI_MIZIK_URL || 'https://funkwhale-server.com'
|
||||||
|
const MIZIK_API_USER = process.env.MIZIK_API_USER || 'user'
|
||||||
|
const MIZIK_API_PASSWORD = process.env.MIZIK_API_PASSWORD || 'password'
|
||||||
|
|
||||||
|
const FORWARDED_HEADERS = ['content-type', 'content-length', 'content-range', 'accept-ranges']
|
||||||
|
|
||||||
|
export async function GET(request, props) {
|
||||||
|
const {id} = await props.params
|
||||||
|
|
||||||
|
const upstreamUrl = `${MIZIK_URL}/rest/stream?u=${encodeURIComponent(MIZIK_API_USER)}&p=${encodeURIComponent(MIZIK_API_PASSWORD)}&id=${encodeURIComponent(id)}`
|
||||||
|
const range = request.headers.get('range')
|
||||||
|
|
||||||
|
const upstreamResponse = await fetch(upstreamUrl, {
|
||||||
|
headers: range ? {range} : {}
|
||||||
|
})
|
||||||
|
|
||||||
|
const headers = new Headers()
|
||||||
|
for (const key of FORWARDED_HEADERS) {
|
||||||
|
const value = upstreamResponse.headers.get(key)
|
||||||
|
if (value) {
|
||||||
|
headers.set(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(upstreamResponse.body, {
|
||||||
|
status: upstreamResponse.status,
|
||||||
|
headers
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import Proptypes from 'prop-types'
|
|
||||||
import {SessionProvider} from 'next-auth/react'
|
|
||||||
|
|
||||||
export default function AuthProvider({children, session}) {
|
|
||||||
return (
|
|
||||||
<SessionProvider session={session}>
|
|
||||||
{children}
|
|
||||||
</SessionProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
AuthProvider.propTypes = {
|
|
||||||
children: Proptypes.node,
|
|
||||||
session: Proptypes.object
|
|
||||||
}
|
|
||||||
@@ -20,7 +20,7 @@ async function jwennAwtis(slug) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function generateMetadata(props) {
|
export async function generateMetadata(props) {
|
||||||
const params = await props.params;
|
const params = await props.params
|
||||||
const {slug} = params
|
const {slug} = params
|
||||||
const anAwtis = await jwennAwtis(slug)
|
const anAwtis = await jwennAwtis(slug)
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ export async function generateMetadata(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default async function AwtisPajSlug(props) {
|
export default async function AwtisPajSlug(props) {
|
||||||
const params = await props.params;
|
const params = await props.params
|
||||||
const {slug} = params
|
const {slug} = params
|
||||||
const anAwtis = await jwennAwtis(slug)
|
const anAwtis = await jwennAwtis(slug)
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -63,7 +63,7 @@ async function jwennDone(paj) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default async function AwitsPaj(props) {
|
export default async function AwitsPaj(props) {
|
||||||
const searchParams = await props.searchParams;
|
const searchParams = await props.searchParams
|
||||||
const {paj} = searchParams
|
const {paj} = searchParams
|
||||||
const {pajTotal, awtisPouChakPaj, pajParsed} = await jwennDone(paj || 1)
|
const {pajTotal, awtisPouChakPaj, pajParsed} = await jwennDone(paj || 1)
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -78,7 +78,7 @@ export default async function RootLayout({children}) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang='fr' suppressHydrationWarning>
|
<html suppressHydrationWarning lang='fr'>
|
||||||
<body>
|
<body>
|
||||||
{plausibleUrl
|
{plausibleUrl
|
||||||
? <PlausibleProvider src={plausibleUrl}>{inner}</PlausibleProvider>
|
? <PlausibleProvider src={plausibleUrl}>{inner}</PlausibleProvider>
|
||||||
|
|||||||
+1
-4
@@ -1,5 +1,3 @@
|
|||||||
export const dynamic = 'force-dynamic'
|
|
||||||
|
|
||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Container from '@mui/material/Container'
|
import Container from '@mui/material/Container'
|
||||||
import {notFound} from 'next/navigation'
|
import {notFound} from 'next/navigation'
|
||||||
@@ -9,7 +7,6 @@ import Statistik from '../components/akey/statistik'
|
|||||||
import Akey from '../components/akey'
|
import Akey from '../components/akey'
|
||||||
import AnVedette from '../components/akey/an-vedette'
|
import AnVedette from '../components/akey/an-vedette'
|
||||||
|
|
||||||
import okiLogo from '../public/logo-512x512.png'
|
|
||||||
import Footer from '../components/footer'
|
import Footer from '../components/footer'
|
||||||
import Aso from '../components/akey/aso'
|
import Aso from '../components/akey/aso'
|
||||||
|
|
||||||
@@ -28,7 +25,7 @@ export default async function Page() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{display: 'flex', flexDirection: 'column', minHeight: '100vh'}}>
|
<Box sx={{display: 'flex', flexDirection: 'column', minHeight: '100vh'}}>
|
||||||
<Akey logo={okiLogo} />
|
<Akey />
|
||||||
<Container sx={{flexGrow: 100}}>
|
<Container sx={{flexGrow: 100}}>
|
||||||
{dernierTeks && <AnVedette teks={dernierTeks} />}
|
{dernierTeks && <AnVedette teks={dernierTeks} />}
|
||||||
<Statistik statistik={statistik} />
|
<Statistik statistik={statistik} />
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import Box from '@mui/material/Box'
|
|||||||
|
|
||||||
import {jwennTeksEpiSlug} from '../../../lib/oki-api'
|
import {jwennTeksEpiSlug} from '../../../lib/oki-api'
|
||||||
import AnTeks from '../../../components/teks/an-teks'
|
import AnTeks from '../../../components/teks/an-teks'
|
||||||
import {getAlias} from '../../../lib/utils/format'
|
import {getAlias} from '../../../lib/utils/get-alias'
|
||||||
import {formatKuveti} from '../../../lib/kuveti'
|
import {formatKuveti} from '../../../lib/kuveti'
|
||||||
import Footer from '../../../components/footer'
|
import Footer from '../../../components/footer'
|
||||||
|
|
||||||
@@ -21,14 +21,14 @@ async function jwennAnTeks(slug) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function generateMetadata(props) {
|
export async function generateMetadata(props) {
|
||||||
const params = await props.params;
|
const params = await props.params
|
||||||
const {slug} = params
|
const {slug} = params
|
||||||
|
|
||||||
const anTeks = await jwennAnTeks(slug)
|
const anTeks = await jwennAnTeks(slug)
|
||||||
|
|
||||||
const awtis = anTeks?.artistes?.length === 1 ? anTeks?.artistes[0].alias : getAlias(anTeks.artistes, anTeks.prioriteArtistes)
|
const awtis = anTeks?.artistes?.length === 1 ? anTeks?.artistes[0].alias : getAlias(anTeks.artistes, anTeks.prioriteArtistes)
|
||||||
const title = `PAWÒL-NU | ${awtis} - ${anTeks.titre}`
|
const title = `PAWÒL-NU | ${awtis} - ${anTeks.titre}`
|
||||||
const description = `Paroles de « ${anTeks?.titre} » : ${anTeks?.transcription.slice(0, 100)}...`
|
const description = `Paroles de « ${anTeks?.titre} » : ${anTeks?.transcription?.slice(0, 100) ?? ''}...`
|
||||||
const url = `${siteUrl}/paroles/${slug}`
|
const url = `${siteUrl}/paroles/${slug}`
|
||||||
|
|
||||||
const {couverture} = anTeks
|
const {couverture} = anTeks
|
||||||
@@ -67,7 +67,7 @@ export async function generateMetadata(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default async function AnPawolPaj(props) {
|
export default async function AnPawolPaj(props) {
|
||||||
const params = await props.params;
|
const params = await props.params
|
||||||
const {slug} = params
|
const {slug} = params
|
||||||
|
|
||||||
const anTeks = await jwennAnTeks(slug)
|
const anTeks = await jwennAnTeks(slug)
|
||||||
|
|||||||
+11
-19
@@ -1,7 +1,7 @@
|
|||||||
import {Suspense} from 'react'
|
import {Suspense} from 'react'
|
||||||
import {notFound} from 'next/navigation'
|
import PropTypes from 'prop-types'
|
||||||
import {jwennTeks} from '../../lib/oki-api'
|
import TeksDrawerLoader from '../../components/teks/teks-drawer-loader'
|
||||||
import TeksDrawer from '../../components/teks/teks-drawer'
|
import {CurrentTrackProvider} from '../../components/teks/current-track-context'
|
||||||
import Loading from './loading'
|
import Loading from './loading'
|
||||||
|
|
||||||
const siteName = process.env.NEXT_PUBLIC_SITE_NAME || 'PAWÒL-NU. Paroles et traductions.'
|
const siteName = process.env.NEXT_PUBLIC_SITE_NAME || 'PAWÒL-NU. Paroles et traductions.'
|
||||||
@@ -39,27 +39,19 @@ export const metadata = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function jwennDotDone() {
|
export default function PawolLayout({children}) {
|
||||||
const teks = await jwennTeks()
|
|
||||||
|
|
||||||
if (!teks) {
|
|
||||||
notFound()
|
|
||||||
}
|
|
||||||
|
|
||||||
return teks
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function PawolLayout({children}) {
|
|
||||||
const teks = await jwennDotDone()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<CurrentTrackProvider>
|
||||||
<Suspense fallback={<Loading />}>
|
<Suspense fallback={<Loading />}>
|
||||||
<TeksDrawer paroles={teks} />
|
<TeksDrawerLoader />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<section>
|
<section>
|
||||||
{children}
|
{children}
|
||||||
</section>
|
</section>
|
||||||
</>
|
</CurrentTrackProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PawolLayout.propTypes = {
|
||||||
|
children: PropTypes.node.isRequired
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
export default function robots() {
|
export default function robots() {
|
||||||
const isProduction = process.env.NEXT_PUBLIC_ENV === 'production';
|
const isProduction = process.env.NODE_ENV === 'production'
|
||||||
|
|
||||||
if (!isProduction) {
|
if (!isProduction) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@ export default function Sipote() {
|
|||||||
<Box sx={{display: 'flex', marginTop: 10, flexDirection: 'column', minHeight: '100vh'}}>
|
<Box sx={{display: 'flex', marginTop: 10, flexDirection: 'column', minHeight: '100vh'}}>
|
||||||
<Box sx={{flexGrow: 1, marginBottom: 10}}>
|
<Box sx={{flexGrow: 1, marginBottom: 10}}>
|
||||||
<Container>
|
<Container>
|
||||||
<Typography gutterBottom sx={{fontWeight: 'bold', textAlign:'center' }} variant='h4' component='h1'>
|
<Typography gutterBottom sx={{fontWeight: 'bold', textAlign: 'center'}} variant='h4' component='h1'>
|
||||||
Soutenir ORGANISATION KA INTERNATIONALE !
|
Soutenir ORGANISATION KA INTERNATIONALE !
|
||||||
</Typography>
|
</Typography>
|
||||||
<Paper sx={{padding: 2, marginTop: 3}}>
|
<Paper sx={{padding: 2, marginTop: 3}}>
|
||||||
|
|||||||
@@ -31,10 +31,6 @@ export default async function sitemap() {
|
|||||||
{
|
{
|
||||||
url: `${url}/awtis`,
|
url: `${url}/awtis`,
|
||||||
priority: 0.6
|
priority: 0.6
|
||||||
},
|
|
||||||
{
|
|
||||||
url: `${url}/pwopose`,
|
|
||||||
priority: 0.5
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -69,13 +69,13 @@ export default function ThemeRegistry(props) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<InitColorSchemeScript attribute='class' />
|
<InitColorSchemeScript attribute='class' />
|
||||||
<NextAppDirEmotionCacheProvider options={{key: 'mui'}}>
|
<NextAppDirEmotionCacheProvider options={{key: 'mui'}}>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<CssBaseline enableColorScheme />
|
<CssBaseline enableColorScheme />
|
||||||
<ChanjeTem />
|
<ChanjeTem />
|
||||||
{children}
|
{children}
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</NextAppDirEmotionCacheProvider>
|
</NextAppDirEmotionCacheProvider>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import Chip from '@mui/material/Chip'
|
|||||||
import Image from 'next/image'
|
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/get-alias'
|
||||||
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'
|
||||||
@@ -36,10 +36,10 @@ export default function AnVedette({teks}) {
|
|||||||
<Box sx={{position: 'relative', width: '100%', aspectRatio: {xs: '1 / 1', sm: '16 / 9'}, maxHeight: {sm: 420}}}>
|
<Box sx={{position: 'relative', width: '100%', aspectRatio: {xs: '1 / 1', sm: '16 / 9'}, maxHeight: {sm: 420}}}>
|
||||||
{fmt?.url ? (
|
{fmt?.url ? (
|
||||||
<Image
|
<Image
|
||||||
src={`${IMAGE_URL}${fmt.url}`}
|
|
||||||
alt={titre}
|
|
||||||
fill
|
fill
|
||||||
priority
|
priority
|
||||||
|
src={`${IMAGE_URL}${fmt.url}`}
|
||||||
|
alt={titre}
|
||||||
placeholder='blur'
|
placeholder='blur'
|
||||||
blurDataURL={BLUR_DATA_URL}
|
blurDataURL={BLUR_DATA_URL}
|
||||||
sizes='(max-width: 600px) 100vw, 800px'
|
sizes='(max-width: 600px) 100vw, 800px'
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
import PropTypes from 'prop-types'
|
|
||||||
import Container from '@mui/material/Container'
|
import Container from '@mui/material/Container'
|
||||||
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 Button from '@mui/material/Button'
|
|
||||||
import Image from 'next/image'
|
|
||||||
import Link from 'next/link'
|
|
||||||
|
|
||||||
export default function Akey({logo}) {
|
export default function Akey() {
|
||||||
return (
|
return (
|
||||||
<Box sx={{flexGrow: 1, marginBottom: 3, marginTop: 7}}>
|
<Box sx={{flexGrow: 1, marginBottom: 3, marginTop: 7}}>
|
||||||
<Container align='center'>
|
<Container align='center'>
|
||||||
@@ -25,7 +20,3 @@ export default function Akey({logo}) {
|
|||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Akey.propTypes = {
|
|
||||||
logo: PropTypes.object.isRequired
|
|
||||||
}
|
|
||||||
|
|||||||
+116
-89
@@ -12,6 +12,7 @@ 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'
|
||||||
@@ -20,10 +21,12 @@ 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 VerifiedIcon from '@mui/icons-material/Verified'
|
||||||
|
|
||||||
import {formatKuveti} from '../../lib/kuveti'
|
import {formatKuveti} from '../../lib/kuveti'
|
||||||
import {StreamButton} from '../streaming-buttons'
|
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'
|
||||||
|
|
||||||
@@ -35,43 +38,48 @@ const sortTeks = paroles => paroles.sort((a, b) => a.titre.localeCompare(b.titre
|
|||||||
|
|
||||||
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, isExclusiveArtist, titrePhare} = 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 hasStreaming = isExclusiveArtist && titrePhare?.streamAudio?.length > 0
|
const hasStreaming = isExclusiveArtist && titrePhare?.streamAudio?.length > 0
|
||||||
const coverUrl = titrePhare?.couverture
|
const coverUrl = titrePhare?.couverture
|
||||||
? `${IMAGE_URL}${titrePhare.couverture.formats?.small?.url || titrePhare.couverture.formats?.thumbnail?.url || titrePhare.couverture.url}`
|
? `${IMAGE_URL}${formatKuveti(titrePhare.couverture, 'small')?.url}`
|
||||||
: null
|
: null
|
||||||
|
|
||||||
const photoUrl = photo?.url
|
const photoUrl = photo?.url
|
||||||
? `${IMAGE_URL}${photo.formats?.small?.url || photo.formats?.thumbnail?.url || photo.url}`
|
? `${IMAGE_URL}${formatKuveti(photo, 'small')?.url}`
|
||||||
: null
|
: null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container maxWidth='sm'>
|
||||||
<Box sx={{mt: 8, mb: 2}}>
|
<Box sx={{mt: 2, mb: 1}}>
|
||||||
<Typography sx={{textAlign: 'center'}} variant='h6' component='h1'>
|
<Link passHref href='/awtis'>
|
||||||
{alias}
|
<Button size='small' variant='text' startIcon={<KeyboardBackspaceIcon />} sx={{color: 'text.secondary'}}>
|
||||||
</Typography>
|
Retour aux artistes
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box sx={{display: 'flex', justifyContent: 'center', mb: 2}}>
|
{/* Héro */}
|
||||||
|
<Box sx={{display: 'flex', flexDirection: 'column', alignItems: 'center', pt: 2, pb: 3}}>
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
width: 200, height: 200,
|
width: 150, height: 150,
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
border: `2px solid ${green[500]}`,
|
border: `3px solid ${green[500]}`,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
}}>
|
mb: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{photoUrl ? (
|
{photoUrl ? (
|
||||||
<Image
|
<Image
|
||||||
src={photoUrl}
|
src={photoUrl}
|
||||||
alt={`Photo ${alias}`}
|
alt={`Photo ${alias}`}
|
||||||
width={200}
|
width={150}
|
||||||
height={200}
|
height={150}
|
||||||
placeholder='blur'
|
placeholder='blur'
|
||||||
blurDataURL={BLUR_DATA_URL}
|
blurDataURL={BLUR_DATA_URL}
|
||||||
style={{objectFit: 'cover', width: '100%', height: '100%'}}
|
style={{objectFit: 'cover', width: '100%', height: '100%'}}
|
||||||
@@ -80,32 +88,44 @@ export default function AwtisDetay({anAwtis}) {
|
|||||||
<Box sx={{width: '100%', height: '100%', bgcolor: 'grey.300'}} />
|
<Box sx={{width: '100%', height: '100%', bgcolor: 'grey.300'}} />
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
<Typography variant='h5' component='h1' sx={{fontWeight: 700, mb: 1, textAlign: 'center'}}>
|
||||||
|
{alias}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{rezoSosyal?.length > 0 && (
|
||||||
|
<Box sx={{display: 'flex', gap: 0.75, flexWrap: 'wrap', justifyContent: 'center', mb: 1.5}}>
|
||||||
|
{rezoSosyal.map(rezo => (
|
||||||
|
<SocialButton key={rezo.url} rezo={rezo} />
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isExclusiveArtist && (
|
||||||
|
<Box sx={{display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 0.5}}>
|
||||||
|
<Chip
|
||||||
|
icon={<VerifiedIcon sx={{fontSize: 16}} />}
|
||||||
|
label={`Artiste ${EXCLUSIVE_LABEL}`}
|
||||||
|
size='small'
|
||||||
|
sx={{bgcolor: '#FFD700', color: '#000', fontWeight: 700, '& .MuiChip-icon': {color: '#000'}}}
|
||||||
|
/>
|
||||||
|
<Typography variant='caption' sx={{color: 'text.secondary', textAlign: 'center', maxWidth: 340}}>
|
||||||
|
Certains morceaux sont publiés en exclusivité sur PAWÒL-NU, avant toute sortie sur les plateformes de streaming.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{isExclusiveArtist && (
|
<Grid container direction='column' spacing={2}>
|
||||||
<Box sx={{display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 0.75, mb: 2}}>
|
|
||||||
<Chip
|
|
||||||
icon={<VerifiedIcon sx={{fontSize: 16}} />}
|
|
||||||
label={`Artiste ${EXCLUSIVE_LABEL}`}
|
|
||||||
size='small'
|
|
||||||
sx={{bgcolor: '#FFD700', color: '#000', fontWeight: 700, '& .MuiChip-icon': {color: '#000'}}}
|
|
||||||
/>
|
|
||||||
<Typography variant='caption' sx={{color: 'text.secondary', textAlign: 'center'}}>
|
|
||||||
Certains morceaux sont publiés en exclusivité sur PAWÒL-NU, avant toute sortie sur les plateformes de streaming.
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Grid sx={{alignItems: 'center'}} container direction='column' spacing={3}>
|
|
||||||
{biyo && (
|
{biyo && (
|
||||||
<Grid size={{xs: 12, md: 6}}>
|
<Grid size={12}>
|
||||||
<Card sx={{minWidth: 300}}>
|
<Card>
|
||||||
<CardActionArea onClick={() => meteEsByografiOuve(true)}>
|
<CardActionArea onClick={() => meteEsByografiOuve(true)}>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Typography gutterBottom variant='body1' component='h2'>
|
<Typography gutterBottom variant='body2' sx={{fontWeight: 700, textTransform: 'uppercase', color: 'text.secondary', letterSpacing: 0.5}}>
|
||||||
<strong>Biographie</strong>
|
Biographie
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography textalign='justify' variant='subtitle1' component='h3'>
|
<Typography variant='body1'>
|
||||||
{biyo}
|
{biyo}
|
||||||
</Typography>
|
</Typography>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -113,36 +133,48 @@ export default function AwtisDetay({anAwtis}) {
|
|||||||
</Card>
|
</Card>
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{hasStreaming && (
|
{hasStreaming && (
|
||||||
<Grid size={{xs: 12, md: 6}}>
|
<Grid size={12}>
|
||||||
<Card sx={{overflow: 'hidden'}}>
|
<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>
|
<Grid container>
|
||||||
{coverUrl && (
|
{coverUrl && (
|
||||||
<Grid size={{xs: 12, sm: 4}}>
|
<Grid size={{xs: 12, sm: 4}}>
|
||||||
<Box sx={{position: 'relative', minHeight: 140, height: '100%'}}>
|
<Link href={`/paroles/${titrePhare.slug}`} style={{display: 'block', height: '100%'}}>
|
||||||
<Image
|
<Box sx={{position: 'relative', minHeight: 130, height: '100%'}}>
|
||||||
src={coverUrl}
|
<Image
|
||||||
alt={titrePhare.titre}
|
fill
|
||||||
fill
|
src={coverUrl}
|
||||||
placeholder='blur'
|
alt={titrePhare.titre}
|
||||||
blurDataURL={BLUR_DATA_URL}
|
placeholder='blur'
|
||||||
sizes='200px'
|
blurDataURL={BLUR_DATA_URL}
|
||||||
style={{objectFit: 'cover'}}
|
sizes='200px'
|
||||||
/>
|
style={{objectFit: 'cover'}}
|
||||||
</Box>
|
/>
|
||||||
|
</Box>
|
||||||
|
</Link>
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
<Grid size={{xs: 12, sm: coverUrl ? 8 : 12}}>
|
<Grid size={{xs: 12, sm: coverUrl ? 8 : 12}}>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Typography variant='subtitle1' sx={{fontWeight: 700}} gutterBottom>
|
<Typography gutterBottom variant='subtitle1' sx={{fontWeight: 700}}>
|
||||||
{titrePhare.titre}
|
<Link href={`/paroles/${titrePhare.slug}`} style={{color: 'inherit', textDecoration: 'none'}}>
|
||||||
|
{titrePhare.titre}
|
||||||
|
</Link>
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant='caption' sx={{color: 'text.secondary', display: 'block', mb: 1.5}}>
|
<Typography variant='caption' sx={{color: 'text.secondary', display: 'block', mb: 1}}>
|
||||||
Écouter sur
|
Écouter sur
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box sx={{display: 'flex', flexWrap: 'wrap', gap: 1}}>
|
<Box sx={{display: 'flex', flexWrap: 'wrap', gap: 1}}>
|
||||||
{titrePhare.streamAudio.map((lyen, i) => (
|
{titrePhare.streamAudio.map(lyen => (
|
||||||
<StreamButton key={i} lyen={lyen} />
|
<StreamButton key={lyen.url} lyen={lyen} />
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -151,47 +183,42 @@ export default function AwtisDetay({anAwtis}) {
|
|||||||
</Card>
|
</Card>
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
<Grid size={{xs: 12, md: 6}}>
|
|
||||||
<Box marginbottom={3}>
|
<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'
|
<Chip color='primary' label={paroles.length} size='small' variant='contained' />
|
||||||
id='panel-teks-header'
|
</AccordionSummary>
|
||||||
>
|
<AccordionDetails sx={{paddingInline: 0}}>
|
||||||
<Typography sx={{marginRight: 2, textAlign: 'center'}} variant='body1' component='h2'><strong>Liste des paroles</strong></Typography>
|
{sortedTeks.map(anPawol => {
|
||||||
<Chip color='primary' label={paroles.length} size='small' variant='contained' />
|
const {couverture} = anPawol
|
||||||
</AccordionSummary>
|
const kuvetiFormat = formatKuveti(couverture, 'thumbnail')
|
||||||
<AccordionDetails sx={{paddingInline: 0}}>
|
return (
|
||||||
{sortedTeks.map(anPawol => {
|
<Box key={anPawol.id} sx={{paddingBlock: 0.5}}>
|
||||||
const {couverture} = anPawol
|
<MizikLyen anPawol={anPawol} kuveti={kuvetiFormat} />
|
||||||
const kuvetiFormat = couverture?.formats?.thumbnail || formatKuveti(couverture)
|
</Box>
|
||||||
return (
|
)
|
||||||
<Box key={anPawol.id} sx={{paddingBlock: 0.5}}>
|
})}
|
||||||
<MizikLyen anPawol={anPawol} kuveti={kuvetiFormat} />
|
</AccordionDetails>
|
||||||
</Box>
|
</Accordion>
|
||||||
)
|
) : (paroles.length === 0 ? (
|
||||||
})}
|
<Typography textAlign='center' variant='body1' color='text.secondary'>
|
||||||
</AccordionDetails>
|
Aucune parole pour le moment
|
||||||
</Accordion>
|
</Typography>
|
||||||
) : (
|
) : (
|
||||||
paroles.length === 0 ? (
|
<Box>
|
||||||
<Typography gutterBottom textAlign='center' variant='body1' component='h2'><strong>Aucune parole pour le moment</strong></Typography>
|
<Typography gutterBottom variant='body1'><strong>Parole</strong></Typography>
|
||||||
) : (
|
<Paper sx={{paddingBlock: 2}}>
|
||||||
<Box>
|
<MizikLyen anPawol={paroles[0]} kuveti={formatKuveti(paroles[0].couverture, 'thumbnail')} />
|
||||||
<Typography gutterBottom textalign='center' variant='body1' component='h2'><strong>Parole</strong></Typography>
|
</Paper>
|
||||||
<Paper sx={{height: '100%', paddingBlock: 2}}>
|
</Box>
|
||||||
<MizikLyen anPawol={paroles[0]} kuveti={paroles[0].couverture?.formats?.thumbnail || formatKuveti(paroles[0].couverture)} />
|
))}
|
||||||
</Paper>
|
|
||||||
</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
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ 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 VerifiedIcon from '@mui/icons-material/Verified'
|
||||||
|
|
||||||
|
import {formatKuveti} from '../../lib/kuveti'
|
||||||
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 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 EXCLUSIVE_LABEL = process.env.NEXT_PUBLIC_EXCLUSIVE_ARTIST_LABEL || 'OKI Exclusif'
|
||||||
|
|
||||||
@@ -82,8 +82,8 @@ export default function AwtisKat({artiste}) {
|
|||||||
className={classes.media}
|
className={classes.media}
|
||||||
component='img'
|
component='img'
|
||||||
alt={alias}
|
alt={alias}
|
||||||
image={`${photo?.url ? `${IMAGE_URL}${photo?.formats?.thumbnail?.url || photo?.url}` : noImageUrl}`}
|
image={`${photo?.url ? `${IMAGE_URL}${formatKuveti(photo, 'thumbnail')?.url}` : noImageUrl}`}
|
||||||
loading='lazy'
|
loading='lazy'
|
||||||
title={alias}
|
title={alias}
|
||||||
/>
|
/>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import {useRouter} from 'next/navigation'
|
|||||||
import TextField from '@mui/material/TextField'
|
import TextField from '@mui/material/TextField'
|
||||||
import Autocomplete from '@mui/material/Autocomplete'
|
import Autocomplete from '@mui/material/Autocomplete'
|
||||||
import Avatar from '@mui/material/Avatar'
|
import Avatar from '@mui/material/Avatar'
|
||||||
import CircularProgress from '@mui/material/CircularProgress'
|
|
||||||
import Container from '@mui/material/Container'
|
import Container from '@mui/material/Container'
|
||||||
|
|
||||||
import {jwennToutAwtis} from '../../lib/oki-api'
|
import {jwennToutAwtis} from '../../lib/oki-api'
|
||||||
|
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'
|
||||||
|
|
||||||
@@ -49,15 +49,13 @@ export default function ChecheAwtis() {
|
|||||||
}
|
}
|
||||||
}, [loading])
|
}, [loading])
|
||||||
|
|
||||||
const sortedOptions = useMemo(() => {
|
const sortedOptions = useMemo(() => [...options].sort((a, b) =>
|
||||||
return [...options].sort((a, b) =>
|
-b.firstLetter.localeCompare(a.firstLetter)
|
||||||
-b.firstLetter.localeCompare(a.firstLetter)
|
), [options])
|
||||||
);
|
|
||||||
}, [options]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
sx={{ display: 'flex', justifyContent: 'center', marginBottom: 3 }}
|
sx={{display: 'flex', justifyContent: 'center', marginBottom: 3}}
|
||||||
>
|
>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
autoHighlight
|
autoHighlight
|
||||||
@@ -69,35 +67,35 @@ export default function ChecheAwtis() {
|
|||||||
noOptionsText='Aucun résultat'
|
noOptionsText='Aucun résultat'
|
||||||
id='cheche-awtis'
|
id='cheche-awtis'
|
||||||
options={sortedOptions}
|
options={sortedOptions}
|
||||||
groupBy={(option) => option?.firstLetter}
|
groupBy={option => option?.firstLetter}
|
||||||
getOptionLabel={(option) => option?.alias}
|
getOptionLabel={option => option?.alias}
|
||||||
renderOption={(props, option) => {
|
renderOption={(optionProps, option) => {
|
||||||
const {key, ...rest} = props;
|
const {key, ...rest} = optionProps
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={key} {...rest}>
|
<li key={key} {...rest}>
|
||||||
<Avatar
|
<Avatar
|
||||||
style={{ marginRight: 8 }}
|
style={{marginRight: 8}}
|
||||||
alt={option?.alias}
|
alt={option?.alias}
|
||||||
src={`${IMAGE_URL}${option?.photo?.formats?.thumbnail?.url || option?.photo?.url || ''}`}
|
src={`${IMAGE_URL}${formatKuveti(option?.photo, 'thumbnail')?.url || ''}`}
|
||||||
/>
|
/>
|
||||||
{option?.alias}
|
{option?.alias}
|
||||||
</li>
|
</li>
|
||||||
);
|
)
|
||||||
}}
|
}}
|
||||||
sx={{ width: 300 }}
|
sx={{width: 300}}
|
||||||
renderInput={(params) => (
|
renderInput={params => (
|
||||||
<TextField
|
<TextField
|
||||||
{...params}
|
{...params}
|
||||||
label='Rechercher un artiste'
|
label='Rechercher un artiste'
|
||||||
slotProps={{
|
slotProps={{
|
||||||
...params.slotProps,
|
...params.slotProps,
|
||||||
htmlInput: { ...params.slotProps.htmlInput },
|
htmlInput: {...params.slotProps.htmlInput},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
onChange={(event, newValue) => {
|
onChange={(event, newValue) => {
|
||||||
router.push(`/awtis/${newValue.slug}`);
|
router.push(`/awtis/${newValue.slug}`)
|
||||||
}}
|
}}
|
||||||
onOpen={() => setOpen(true)}
|
onOpen={() => setOpen(true)}
|
||||||
onClose={() => setOpen(false)}
|
onClose={() => setOpen(false)}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export default function MizikLis({niAwtis, paroles, meteEsMobilOuve}) {
|
|||||||
itemContent={index => {
|
itemContent={index => {
|
||||||
const anPawol = pawol[index]
|
const anPawol = pawol[index]
|
||||||
const {couverture} = anPawol
|
const {couverture} = anPawol
|
||||||
const kuvetiFormat = couverture?.formats?.thumbnail || formatKuveti(couverture)
|
const kuvetiFormat = formatKuveti(couverture, 'thumbnail')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MizikLyen niAwtis={niAwtis} anPawol={anPawol} kuveti={kuvetiFormat} slug={params.slug} meteEsMobilOuve={meteEsMobilOuve} />
|
<MizikLyen niAwtis={niAwtis} anPawol={anPawol} kuveti={kuvetiFormat} slug={params.slug} meteEsMobilOuve={meteEsMobilOuve} />
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import LibraryMusicIcon from '@mui/icons-material/LibraryMusic'
|
|||||||
import ExplicitIcon from '@mui/icons-material/Explicit'
|
import ExplicitIcon from '@mui/icons-material/Explicit'
|
||||||
|
|
||||||
import {esBrandNew} from '../../lib/date'
|
import {esBrandNew} from '../../lib/date'
|
||||||
import {getAlias} from '../../lib/utils/format'
|
import {getAlias} from '../../lib/utils/get-alias'
|
||||||
|
|
||||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL_ROOT || 'http://localhost:1337'
|
const apiUrl = process.env.NEXT_PUBLIC_API_URL_ROOT || 'http://localhost:1337'
|
||||||
|
|
||||||
@@ -20,6 +20,7 @@ export default function MizikLyen({niAwtis, anPawol, kuveti, slug, meteEsMobilOu
|
|||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
passHref
|
passHref
|
||||||
|
prefetch={false}
|
||||||
href={`/paroles/${anPawol.slug}#${anPawol.slug}`}
|
href={`/paroles/${anPawol.slug}#${anPawol.slug}`}
|
||||||
style={{textDecoration: 'none', width: '100%', display: 'flex', alignItems: 'center'}}
|
style={{textDecoration: 'none', width: '100%', display: 'flex', alignItems: 'center'}}
|
||||||
onClick={() => meteEsMobilOuve(false)}
|
onClick={() => meteEsMobilOuve(false)}
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import Chip from '@mui/material/Chip'
|
||||||
|
import IconButton from '@mui/material/IconButton'
|
||||||
|
import Tooltip from '@mui/material/Tooltip'
|
||||||
|
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
|
||||||
|
clickable
|
||||||
|
label={config.label}
|
||||||
|
component='a'
|
||||||
|
href={rezo.url}
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
size='small'
|
||||||
|
sx={{bgcolor: config.bg, color: config.color, fontWeight: 600}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
SocialButton.propTypes = {
|
||||||
|
rezo: PropTypes.shape({
|
||||||
|
plateforme: PropTypes.string.isRequired,
|
||||||
|
url: PropTypes.string.isRequired
|
||||||
|
}).isRequired
|
||||||
|
}
|
||||||
@@ -57,7 +57,7 @@ export default function LicenseModal({license, sourceOriginale, remixes}) {
|
|||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
{sourceOriginale && (
|
{sourceOriginale && (
|
||||||
<Box sx={{mb: 3}}>
|
<Box sx={{mb: 3}}>
|
||||||
<Typography variant='overline' color='text.secondary' display='block' gutterBottom>
|
<Typography gutterBottom variant='overline' color='text.secondary' display='block'>
|
||||||
Basé sur
|
Basé sur
|
||||||
</Typography>
|
</Typography>
|
||||||
<Link href={`/paroles/${sourceOriginale.slug}`} underline='hover' color='inherit'>
|
<Link href={`/paroles/${sourceOriginale.slug}`} underline='hover' color='inherit'>
|
||||||
@@ -76,7 +76,7 @@ export default function LicenseModal({license, sourceOriginale, remixes}) {
|
|||||||
)}
|
)}
|
||||||
{remixes && (
|
{remixes && (
|
||||||
<Box sx={{mb: 3}}>
|
<Box sx={{mb: 3}}>
|
||||||
<Typography variant='overline' color='text.secondary' display='block' gutterBottom>
|
<Typography gutterBottom variant='overline' color='text.secondary' display='block'>
|
||||||
Déclinaisons
|
Déclinaisons
|
||||||
</Typography>
|
</Typography>
|
||||||
{remixes.map(remix => (
|
{remixes.map(remix => (
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -530,7 +530,7 @@ export default function Cgu() {
|
|||||||
10. Propriété intellectuelle et licence
|
10. Propriété intellectuelle et licence
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography paragraph='true' gutterBottom variant='body1'>
|
<Typography gutterBottom paragraph='true' variant='body1'>
|
||||||
Le site internet <Link href='https://pawol.nu'><strong>pawol.nu</strong></Link> et les éléments qui y sont accessibles
|
Le site internet <Link href='https://pawol.nu'><strong>pawol.nu</strong></Link> et les éléments qui y sont accessibles
|
||||||
(textes, images, graphismes, logos, vidéos, icônes, sons, etc.) sont, sauf mention contraire,
|
(textes, images, graphismes, logos, vidéos, icônes, sons, etc.) sont, sauf mention contraire,
|
||||||
mis à disposition sous la licence <Link href='https://www.gnu.org/licenses/agpl-3.0.html' target='_blank' rel='noopener noreferrer'> <strong>GNU Affero General Public License Version 3 (AGPL-3.0)</strong></Link>. Cette licence garantit aux utilisateurs les libertés suivantes :
|
mis à disposition sous la licence <Link href='https://www.gnu.org/licenses/agpl-3.0.html' target='_blank' rel='noopener noreferrer'> <strong>GNU Affero General Public License Version 3 (AGPL-3.0)</strong></Link>. Cette licence garantit aux utilisateurs les libertés suivantes :
|
||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -70,12 +70,14 @@ export default function FilesList({files}) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const audioFiles = files.filter(f => f.mime.startsWith('audio'))
|
const audioFiles = files.filter(f => f.mime.startsWith('audio'))
|
||||||
if (audioFiles.length === 0) return
|
if (audioFiles.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
|
|
||||||
async function fetchAllMeta() {
|
async function fetchAllMeta() {
|
||||||
const mm = await import('music-metadata-browser')
|
const mm = await import('music-metadata-browser') // eslint-disable-line node/no-unsupported-features/es-syntax
|
||||||
const results = {}
|
const results = {}
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
audioFiles.map(async file => {
|
audioFiles.map(async file => {
|
||||||
@@ -98,7 +100,9 @@ export default function FilesList({files}) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
if (!cancelled) setAudioMeta(results)
|
if (!cancelled) {
|
||||||
|
setAudioMeta(results)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchAllMeta()
|
fetchAllMeta()
|
||||||
@@ -121,7 +125,8 @@ export default function FilesList({files}) {
|
|||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
letterSpacing: '0.05rem',
|
letterSpacing: '0.05rem',
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
}}>{isHaute ? 'Haute' : 'Faible'}</Typography>
|
}}
|
||||||
|
>{isHaute ? 'Haute' : 'Faible'}</Typography>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,12 +155,16 @@ export default function FilesList({files}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => () => {
|
useEffect(() => () => {
|
||||||
Object.values(controllersRef.current).forEach(c => c.abort())
|
for (const c of Object.values(controllersRef.current)) {
|
||||||
|
c.abort()
|
||||||
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleClick = async (e, url, fileName, fileId) => {
|
const handleClick = async (e, url, fileName, fileId) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
if (fileId in downloading) return
|
if (fileId in downloading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
controllersRef.current[fileId] = controller
|
controllersRef.current[fileId] = controller
|
||||||
@@ -163,14 +172,18 @@ export default function FilesList({files}) {
|
|||||||
setDownloading(prev => ({...prev, [fileId]: 0}))
|
setDownloading(prev => ({...prev, [fileId]: 0}))
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url, {signal: controller.signal})
|
const response = await fetch(url, {signal: controller.signal})
|
||||||
const contentLength = +response.headers.get('content-length')
|
const contentLength = Number(response.headers.get('content-length'))
|
||||||
const reader = response.body.getReader()
|
const reader = response.body.getReader()
|
||||||
const chunks = []
|
const chunks = []
|
||||||
let received = 0
|
let received = 0
|
||||||
|
|
||||||
while (true) {
|
for (;;) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop -- chaque chunk dépend du précédent, la lecture est intrinsèquement séquentielle
|
||||||
const {done, value} = await reader.read()
|
const {done, value} = await reader.read()
|
||||||
if (done) break
|
if (done) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
chunks.push(value)
|
chunks.push(value)
|
||||||
received += value.length
|
received += value.length
|
||||||
if (contentLength) {
|
if (contentLength) {
|
||||||
@@ -186,7 +199,9 @@ export default function FilesList({files}) {
|
|||||||
a.click()
|
a.click()
|
||||||
URL.revokeObjectURL(blobUrl)
|
URL.revokeObjectURL(blobUrl)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.name !== 'AbortError') throw error
|
if (error.name !== 'AbortError') {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
delete controllersRef.current[fileId]
|
delete controllersRef.current[fileId]
|
||||||
setDownloading(prev => {
|
setDownloading(prev => {
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import {useState} from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import Button from '@mui/material/Button'
|
||||||
|
import Dialog from '@mui/material/Dialog'
|
||||||
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
|
import DialogContent from '@mui/material/DialogContent'
|
||||||
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
|
import List from '@mui/material/List'
|
||||||
|
import ListItem from '@mui/material/ListItem'
|
||||||
|
import ListItemButton from '@mui/material/ListItemButton'
|
||||||
|
import ListItemIcon from '@mui/material/ListItemIcon'
|
||||||
|
import ListItemText from '@mui/material/ListItemText'
|
||||||
|
import useMediaQuery from '@mui/material/useMediaQuery'
|
||||||
|
import {useTheme} from '@mui/material/styles'
|
||||||
|
|
||||||
|
import CloudDownloadIcon from '@mui/icons-material/CloudDownload'
|
||||||
|
import {Nextcloud} from '@icons-pack/react-simple-icons'
|
||||||
|
|
||||||
|
const PLATFORM_ICON = {
|
||||||
|
NIYAJ: Nextcloud,
|
||||||
|
kDrive: CloudDownloadIcon,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function KitsDialog({kits}) {
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const theme = useTheme()
|
||||||
|
const fullScreen = useMediaQuery(theme.breakpoints.down('md'))
|
||||||
|
|
||||||
|
const handleClickOpen = () => {
|
||||||
|
setOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button endIcon={<CloudDownloadIcon />} sx={{marginBottom: 2}} variant='outlined' onClick={handleClickOpen}>
|
||||||
|
Kits
|
||||||
|
</Button>
|
||||||
|
<Dialog
|
||||||
|
fullScreen={fullScreen}
|
||||||
|
open={open}
|
||||||
|
aria-labelledby='KitsDialog'
|
||||||
|
onClose={handleClose}
|
||||||
|
>
|
||||||
|
<DialogTitle>Télécharger un kit</DialogTitle>
|
||||||
|
<DialogContent dividers>
|
||||||
|
<List>
|
||||||
|
{kits.map(kit => {
|
||||||
|
const PlatformIcon = PLATFORM_ICON[kit.plateforme] ?? CloudDownloadIcon
|
||||||
|
return (
|
||||||
|
<ListItem key={kit.url} disablePadding>
|
||||||
|
<ListItemButton component='a' href={kit.url} target='_blank' rel='noopener noreferrer'>
|
||||||
|
<ListItemIcon>
|
||||||
|
<PlatformIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={kit.type} secondary={kit.plateforme} />
|
||||||
|
</ListItemButton>
|
||||||
|
</ListItem>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</List>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button autoFocus onClick={handleClose}>
|
||||||
|
Fermer
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
KitsDialog.propTypes = {
|
||||||
|
kits: PropTypes.array.isRequired
|
||||||
|
}
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
import {useCallback, useEffect, useState, forwardRef} from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import axios from 'axios'
|
|
||||||
import {
|
|
||||||
TextField,
|
|
||||||
Container,
|
|
||||||
Button,
|
|
||||||
Snackbar,
|
|
||||||
LinearProgress,
|
|
||||||
Typography
|
|
||||||
} from '@mui/material'
|
|
||||||
import MuiAlert from '@mui/material/Alert'
|
|
||||||
|
|
||||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:1337/api'
|
|
||||||
|
|
||||||
const Alert = forwardRef(function Alert(props, ref) {
|
|
||||||
return <MuiAlert ref={ref} elevation={6} variant='filled' {...props} />
|
|
||||||
})
|
|
||||||
function EkriKomante({session, paroleId, meteEsKomanteOuve}) {
|
|
||||||
const {jwt, user} = session
|
|
||||||
const [komante, meteKomante] = useState({kontni: ''})
|
|
||||||
const [ere, meteEre] = useState('')
|
|
||||||
const [sikse, meteSikse] = useState('')
|
|
||||||
const [chaje, meteChaje] = useState(false)
|
|
||||||
const [esOuve, meteEsOuve] = useState(false)
|
|
||||||
|
|
||||||
const handleClick = async () => {
|
|
||||||
meteChaje(true)
|
|
||||||
const {kontni} = komante
|
|
||||||
|
|
||||||
if (kontni === '') {
|
|
||||||
meteEre({error: {message: 'Champ obligatoire'}})
|
|
||||||
meteChaje(false)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const headers = {
|
|
||||||
'content-type': 'application/json',
|
|
||||||
Authorization: `Bearer ${jwt}`
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await axios.post(`${API_URL}/commentaires`, {
|
|
||||||
data: {
|
|
||||||
contenu: kontni,
|
|
||||||
parole: paroleId,
|
|
||||||
user: {
|
|
||||||
id: user.id,
|
|
||||||
username: user.username,
|
|
||||||
email: user.email
|
|
||||||
},
|
|
||||||
datePublication: new Date()
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
headers
|
|
||||||
})
|
|
||||||
|
|
||||||
meteSikse('Commentaire envoyé avec succès. Il apparaîtra sur le site après validation.')
|
|
||||||
meteChaje(false)
|
|
||||||
} catch (error) {
|
|
||||||
meteEre(error?.response?.data)
|
|
||||||
meteChaje(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleUpdate = useCallback(update => {
|
|
||||||
meteKomante({...komante, ...update})
|
|
||||||
}, [komante])
|
|
||||||
|
|
||||||
const handleClose = (event, reason) => {
|
|
||||||
if (reason === 'clickaway') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
meteEsOuve(false)
|
|
||||||
meteSikse('')
|
|
||||||
meteEre('')
|
|
||||||
meteEsKomanteOuve(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleReset = () => {
|
|
||||||
meteKomante({kontni: ''})
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (sikse) {
|
|
||||||
meteEsOuve(true)
|
|
||||||
handleReset()
|
|
||||||
}
|
|
||||||
}, [sikse])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (ere) {
|
|
||||||
meteEsOuve(true)
|
|
||||||
}
|
|
||||||
}, [ere])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container maxWidth='sm'>
|
|
||||||
<form noValidate autoComplete='off'>
|
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
multiline
|
|
||||||
required
|
|
||||||
helperText='*Obligatoire. 500 caractères maximum'
|
|
||||||
style={{marginTop: '1em'}}
|
|
||||||
id='komante'
|
|
||||||
label='Commentaire'
|
|
||||||
value={komante.kontni}
|
|
||||||
rows={8}
|
|
||||||
variant='outlined'
|
|
||||||
onChange={event => handleUpdate({kontni: event.target.value})}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
fullWidth
|
|
||||||
disabled={chaje || komante.komante === ''}
|
|
||||||
style={{marginBlock: 20}}
|
|
||||||
variant='contained'
|
|
||||||
color='primary'
|
|
||||||
onClick={handleClick}
|
|
||||||
>
|
|
||||||
<Typography style={{fontWeight: 'bold'}}>
|
|
||||||
Valider
|
|
||||||
</Typography>
|
|
||||||
</Button>
|
|
||||||
{chaje && <LinearProgress size={24} style={{width: '100%', marginBlock: '1em'}} />}
|
|
||||||
</div>
|
|
||||||
{sikse && (
|
|
||||||
<Snackbar open={esOuve} autoHideDuration={10_000} onClose={handleClose}>
|
|
||||||
<Alert severity='success' onClose={handleClose}>
|
|
||||||
<strong>{sikse}</strong>
|
|
||||||
</Alert>
|
|
||||||
</Snackbar>
|
|
||||||
)}
|
|
||||||
{ere && (
|
|
||||||
<Snackbar open={esOuve} autoHideDuration={10_000} onClose={handleClose}>
|
|
||||||
<Alert severity='error' onClose={handleClose}>
|
|
||||||
<strong>Une erreur s’est produite</strong> : <i>{ere?.error?.message}</i>
|
|
||||||
</Alert>
|
|
||||||
</Snackbar>
|
|
||||||
)}
|
|
||||||
</Container>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
EkriKomante.propTypes = {
|
|
||||||
session: PropTypes.object.isRequired,
|
|
||||||
paroleId: PropTypes.number.isRequired,
|
|
||||||
meteEsKomanteOuve: PropTypes.func.isRequired
|
|
||||||
}
|
|
||||||
|
|
||||||
export default EkriKomante
|
|
||||||
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import PropTypes from 'prop-types'
|
|
||||||
import {format} from 'date-fns'
|
|
||||||
import {fr} from 'date-fns/locale'
|
|
||||||
import {styled} from '@mui/material/styles'
|
|
||||||
|
|
||||||
import {
|
|
||||||
Typography,
|
|
||||||
Divider,
|
|
||||||
List,
|
|
||||||
ListItemText
|
|
||||||
} from '@mui/material'
|
|
||||||
|
|
||||||
import {formatJsonString} from '../../lib/utils/format'
|
|
||||||
|
|
||||||
const PREFIX = 'komante-list'
|
|
||||||
|
|
||||||
const classes = {
|
|
||||||
root: `${PREFIX}-root`,
|
|
||||||
inline: `${PREFIX}-inline`
|
|
||||||
}
|
|
||||||
|
|
||||||
const StyledList = styled(List)((
|
|
||||||
{
|
|
||||||
theme
|
|
||||||
}
|
|
||||||
) => ({
|
|
||||||
[`&.${classes.root}`]: {
|
|
||||||
width: '100%',
|
|
||||||
maxWidth: '36ch',
|
|
||||||
backgroundColor: theme.palette.background.paper
|
|
||||||
},
|
|
||||||
|
|
||||||
[`& .${classes.inline}`]: {
|
|
||||||
display: 'inline'
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
export default function KomanteList({commentaires}) {
|
|
||||||
return (
|
|
||||||
<StyledList className={classes.root}>
|
|
||||||
{commentaires.map(({id, user, contenu, datePublication}) => (
|
|
||||||
<div key={id}>
|
|
||||||
<ListItemText
|
|
||||||
primary={
|
|
||||||
<Typography gutterBottom style={{textDecoration: 'underline'}} variant='body1'>
|
|
||||||
<small>{user.username}</small>
|
|
||||||
</Typography>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<ListItemText
|
|
||||||
primary={formatJsonString(contenu)}
|
|
||||||
secondary={
|
|
||||||
<Typography gutterBottom style={{marginBlock: 5, fontStyle: 'italic'}} variant='caption' display='block'>
|
|
||||||
{format(new Date(datePublication), 'Pp', {locale: fr})}
|
|
||||||
</Typography>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Divider style={{marginBottom: '1em'}} />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</StyledList>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
KomanteList.propTypes = {
|
|
||||||
commentaires: PropTypes.array.isRequired
|
|
||||||
}
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
import {useState, useEffect, useRef} from 'react'
|
|
||||||
import {styled} from '@mui/material/styles'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import {useSession} from 'next-auth/react'
|
|
||||||
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogTitle,
|
|
||||||
DialogContent,
|
|
||||||
DialogActions,
|
|
||||||
Button,
|
|
||||||
Typography,
|
|
||||||
Badge
|
|
||||||
} from '@mui/material'
|
|
||||||
import {useRouter} from 'next/navigation'
|
|
||||||
import Koneksyon from '../sesyon/koneksyon'
|
|
||||||
import KomanteList from './komante-list'
|
|
||||||
import EkriKomante from './ekri-komante'
|
|
||||||
|
|
||||||
const PREFIX = 'vwe-komante'
|
|
||||||
|
|
||||||
const classes = {
|
|
||||||
tooltip: `${PREFIX}-tooltip`,
|
|
||||||
margin: `${PREFIX}-margin`,
|
|
||||||
extendedIcon: `${PREFIX}-extendedIcon`
|
|
||||||
}
|
|
||||||
|
|
||||||
const Root = styled('div')((
|
|
||||||
{
|
|
||||||
theme
|
|
||||||
}
|
|
||||||
) => ({
|
|
||||||
[`& .${classes.margin}`]: {
|
|
||||||
margin: theme.spacing(1)
|
|
||||||
},
|
|
||||||
|
|
||||||
[`& .${classes.extendedIcon}`]: {
|
|
||||||
marginRight: theme.spacing(1)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
export default function VweKomante({commentaires, parole, paroleId}) {
|
|
||||||
const [esOuve, meteEsOuve] = useState(false)
|
|
||||||
const [esKoneksyonOuve, meteEsKoneksyonOuve] = useState(false)
|
|
||||||
const [esKomenteOuve, meteEsKomanteOuve] = useState(false)
|
|
||||||
const {data: session} = useSession()
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const handleClick = () => {
|
|
||||||
meteEsOuve(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
meteEsOuve(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const descriptionElementRef = useRef(null)
|
|
||||||
useEffect(() => {
|
|
||||||
if (esOuve) {
|
|
||||||
const {current: descriptionElement} = descriptionElementRef
|
|
||||||
if (descriptionElement !== null) {
|
|
||||||
descriptionElement.focus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [esOuve])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Root>
|
|
||||||
<Badge badgeContent={commentaires.length} color='primary'>
|
|
||||||
<Button sx={{marginBlock: 3}} variant='outlined' onClick={handleClick}>
|
|
||||||
Voir les commentaires
|
|
||||||
</Button>
|
|
||||||
</Badge>
|
|
||||||
<Dialog
|
|
||||||
open={esOuve}
|
|
||||||
scroll='paper'
|
|
||||||
aria-labelledby='scroll-dialog-title'
|
|
||||||
aria-describedby='scroll-dialog-description'
|
|
||||||
onClose={handleClose}
|
|
||||||
>
|
|
||||||
<DialogTitle align='center' id='scroll-dialog-title'>Commentaires</DialogTitle>
|
|
||||||
<DialogContent dividers>
|
|
||||||
<Typography
|
|
||||||
ref={descriptionElementRef}
|
|
||||||
variant='inherit'
|
|
||||||
id='scroll-dialog-description'
|
|
||||||
tabIndex={-1}
|
|
||||||
component='div'
|
|
||||||
>
|
|
||||||
{commentaires.length > 0 ? (
|
|
||||||
<KomanteList commentaires={commentaires} />
|
|
||||||
) : (
|
|
||||||
<Typography component='div' align='center'>
|
|
||||||
Aucun commentaire
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</Typography>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions align='center' >
|
|
||||||
{session && session.user && !esKomenteOuve && (
|
|
||||||
<Button fullWidth color='primary' onClick={() => meteEsKomanteOuve(true)}>
|
|
||||||
Ajouter un commentaire
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{!session && !esKoneksyonOuve && (
|
|
||||||
<Button fullWidth color='primary' onClick={() => meteEsKoneksyonOuve(true)}>
|
|
||||||
Se connecter pour commenter
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{!session && esKoneksyonOuve && (
|
|
||||||
<Koneksyon chimen={router.asPath} />
|
|
||||||
)}
|
|
||||||
</DialogActions>
|
|
||||||
{!session && esKoneksyonOuve && (
|
|
||||||
<Button style={{marginBlock: 10}} color='secondary' onClick={() => meteEsKoneksyonOuve(false)}>
|
|
||||||
<strong>Annuler</strong>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{session && session.user && esKomenteOuve && (
|
|
||||||
<>
|
|
||||||
<EkriKomante session={session} parole={parole} paroleId={paroleId} meteEsKomanteOuve={meteEsKomanteOuve} />
|
|
||||||
<Button fullWidth style={{marginBottom: 10}} color='primary' onClick={() => meteEsKomanteOuve(false)}>
|
|
||||||
Fermer
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Dialog>
|
|
||||||
</Root>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
VweKomante.propTypes = {
|
|
||||||
commentaires: PropTypes.array,
|
|
||||||
parole: PropTypes.object.isRequired,
|
|
||||||
paroleId: PropTypes.number.isRequired
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,11 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import {useState} from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import {useRouter, usePathname} from 'next/navigation'
|
import {useRouter, usePathname} from 'next/navigation'
|
||||||
|
|
||||||
import {styled} from '@mui/material/styles'
|
import {styled} from '@mui/material/styles'
|
||||||
import AppBar from '@mui/material/AppBar'
|
import AppBar from '@mui/material/AppBar'
|
||||||
import Tabs, {tabsClasses} from '@mui/material/Tabs'
|
import Tabs, {tabsClasses} from '@mui/material/Tabs'
|
||||||
import Tab from '@mui/material/Tab'
|
import Tab from '@mui/material/Tab'
|
||||||
import Typography from '@mui/material/Typography'
|
|
||||||
import Box from '@mui/material/Box'
|
|
||||||
import {useMediaQuery} from '@mui/material'
|
import {useMediaQuery} from '@mui/material'
|
||||||
|
|
||||||
import MusicNoteIcon from '@mui/icons-material/MusicNote'
|
import MusicNoteIcon from '@mui/icons-material/MusicNote'
|
||||||
@@ -49,32 +45,6 @@ const tabPath = {
|
|||||||
awtis: 3,
|
awtis: 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
function TabPanel(props) {
|
|
||||||
const {children, value, index, ...other} = props
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
role='tabpanel'
|
|
||||||
hidden={value !== index}
|
|
||||||
id={`scrollable-force-tabpanel-${index}`}
|
|
||||||
aria-labelledby={`scrollable-force-tab-${index}`}
|
|
||||||
{...other}
|
|
||||||
>
|
|
||||||
{value === index && (
|
|
||||||
<Box p={3}>
|
|
||||||
<Typography component='div'>{children}</Typography>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
TabPanel.propTypes = {
|
|
||||||
children: PropTypes.node,
|
|
||||||
index: PropTypes.any.isRequired,
|
|
||||||
value: PropTypes.any.isRequired
|
|
||||||
}
|
|
||||||
|
|
||||||
function a11yProps(index) {
|
function a11yProps(index) {
|
||||||
return {
|
return {
|
||||||
id: `scrollable-force-tab-${index}`,
|
id: `scrollable-force-tab-${index}`,
|
||||||
@@ -89,11 +59,8 @@ export default function Navigasyon() {
|
|||||||
const selectedTab = tabPath[selectedPath]
|
const selectedTab = tabPath[selectedPath]
|
||||||
|
|
||||||
const isMobile = useMediaQuery('(max-width:422px)')
|
const isMobile = useMediaQuery('(max-width:422px)')
|
||||||
const [value, setValue] = useState(0)
|
|
||||||
|
|
||||||
const handleChange = (event, newValue) => {
|
const handleChange = (event, newValue) => {
|
||||||
setValue(newValue)
|
|
||||||
|
|
||||||
if (newValue !== selectedTab) {
|
if (newValue !== selectedTab) {
|
||||||
router.push(tabRouteHref[newValue])
|
router.push(tabRouteHref[newValue])
|
||||||
}
|
}
|
||||||
@@ -103,7 +70,8 @@ export default function Navigasyon() {
|
|||||||
<Appdiv>
|
<Appdiv>
|
||||||
<div className={classes.root}>
|
<div className={classes.root}>
|
||||||
<AppBar style={{zIndex: 1, boxShadow: 'none'}} position='fixed' color='default'>
|
<AppBar style={{zIndex: 1, boxShadow: 'none'}} position='fixed' color='default'>
|
||||||
{selectedTab !== undefined && <Tabs
|
{selectedTab !== undefined && (
|
||||||
|
<Tabs
|
||||||
allowScrollButtonsMobile
|
allowScrollButtonsMobile
|
||||||
scrollButtons
|
scrollButtons
|
||||||
centered={Boolean(!isMobile)}
|
centered={Boolean(!isMobile)}
|
||||||
@@ -124,12 +92,8 @@ export default function Navigasyon() {
|
|||||||
<Tab icon={<MusicNoteIcon />} aria-label='Paroles' {...a11yProps(2)} />
|
<Tab icon={<MusicNoteIcon />} aria-label='Paroles' {...a11yProps(2)} />
|
||||||
<Tab icon={<RecordVoiceOverIcon />} aria-label='Artistes' {...a11yProps(3)} />
|
<Tab icon={<RecordVoiceOverIcon />} aria-label='Artistes' {...a11yProps(3)} />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
}
|
)}
|
||||||
</AppBar>
|
</AppBar>
|
||||||
<TabPanel value={value} index={0} />
|
|
||||||
<TabPanel value={value} index={1} />
|
|
||||||
<TabPanel value={value} index={2} />
|
|
||||||
<TabPanel value={value} index={3} />
|
|
||||||
</div>
|
</div>
|
||||||
</Appdiv>
|
</Appdiv>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,202 +0,0 @@
|
|||||||
import {useState, forwardRef} from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import axios from 'axios'
|
|
||||||
import {useRouter} from 'next/navigation'
|
|
||||||
|
|
||||||
import {FormControl, Snackbar, IconButton, Button, Input, InputAdornment, InputLabel, Box, Container, Typography, LinearProgress} from '@mui/material'
|
|
||||||
import MuiAlert from '@mui/material/Alert'
|
|
||||||
import Visibility from '@mui/icons-material/Visibility'
|
|
||||||
import VisibilityOff from '@mui/icons-material/VisibilityOff'
|
|
||||||
import Link from '@mui/material/Link'
|
|
||||||
|
|
||||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:1337'
|
|
||||||
|
|
||||||
const Alert = forwardRef(function Alert(props, ref) {
|
|
||||||
return <MuiAlert ref={ref} elevation={6} variant='filled' {...props} />
|
|
||||||
})
|
|
||||||
|
|
||||||
export default function NewPassword({code}) {
|
|
||||||
const router = useRouter()
|
|
||||||
const [showPassword, setShowPassword] = useState('')
|
|
||||||
const [showPasswordConfirmation, setShowPasswordConfirmation] = useState('')
|
|
||||||
const [password, setPassword] = useState('')
|
|
||||||
const [passwordConfirmation, setPasswordConfirmation] = useState('')
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
const [error, setError] = useState('')
|
|
||||||
const [open, setOpen] = useState(true)
|
|
||||||
const [passwordSuccess, setPasswordSuccess] = useState(false)
|
|
||||||
|
|
||||||
const resetForm = () => {
|
|
||||||
setPassword('')
|
|
||||||
setPasswordConfirmation('')
|
|
||||||
}
|
|
||||||
|
|
||||||
const changePassword = async () => {
|
|
||||||
setLoading(true)
|
|
||||||
try {
|
|
||||||
await axios.post(`${API_URL}/auth/reset-password`, {
|
|
||||||
code,
|
|
||||||
password,
|
|
||||||
passwordConfirmation
|
|
||||||
})
|
|
||||||
setLoading(false)
|
|
||||||
setPasswordSuccess(true)
|
|
||||||
setTimeout(() => {
|
|
||||||
router.push('/pwopose')
|
|
||||||
}, 10_000)
|
|
||||||
} catch {
|
|
||||||
setOpen(true)
|
|
||||||
setError('Une erreur s’est produite. Veuillez réessayer ultérieurement')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleMouseDownPassword = event => {
|
|
||||||
event.preventDefault()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleMouseDownPasswordConfirmation = event => {
|
|
||||||
event.preventDefault()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleKeyUpPassword = event => {
|
|
||||||
if (event.keyCode === 13) {
|
|
||||||
handleClick()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleKeyUpPasswordConfirmation = event => {
|
|
||||||
if (event.keyCode === 13) {
|
|
||||||
handleClick()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleClick = async () => {
|
|
||||||
if (password !== passwordConfirmation) {
|
|
||||||
setOpen(true)
|
|
||||||
setError('Les 2 mots de passe de correspondent pas')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (password.length < 6) {
|
|
||||||
setOpen(true)
|
|
||||||
setError('Le mot de passe est trop court, 6 caratères minimum')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await changePassword()
|
|
||||||
setLoading(false)
|
|
||||||
resetForm()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleClose = (event, reason) => {
|
|
||||||
if (reason === 'clickaway') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setOpen(false)
|
|
||||||
setError('')
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container sx={{marginTop: 2}} maxWidth='sm'>
|
|
||||||
<Box sx={{width: '100%'}}>
|
|
||||||
<Typography sx={{textAlign: 'center'}} variant='h5' component='div'>Nouveau mot de passe</Typography>
|
|
||||||
<FormControl fullWidth style={{marginTop: '1em'}}>
|
|
||||||
<InputLabel htmlFor='password'>Mot de passe</InputLabel>
|
|
||||||
<Input
|
|
||||||
value={password}
|
|
||||||
name='register-password'
|
|
||||||
type={showPassword ? 'text' : 'password'}
|
|
||||||
id='register-password'
|
|
||||||
endAdornment={
|
|
||||||
<InputAdornment position='end'>
|
|
||||||
<IconButton
|
|
||||||
aria-label='password visibility'
|
|
||||||
size='large'
|
|
||||||
onClick={() => setShowPassword(!showPassword)}
|
|
||||||
onMouseDown={handleMouseDownPassword}
|
|
||||||
>
|
|
||||||
{showPassword ? <Visibility /> : <VisibilityOff />}
|
|
||||||
</IconButton>
|
|
||||||
</InputAdornment>
|
|
||||||
}
|
|
||||||
onChange={event => setPassword(event.target.value)}
|
|
||||||
onKeyUp={handleKeyUpPassword}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormControl fullWidth style={{marginTop: '1em'}}>
|
|
||||||
<InputLabel htmlFor='password'>Vérification du mot de passe</InputLabel>
|
|
||||||
<Input
|
|
||||||
value={passwordConfirmation}
|
|
||||||
name='verification-password'
|
|
||||||
type={showPasswordConfirmation ? 'text' : 'password'}
|
|
||||||
id='verification-password'
|
|
||||||
error={password !== passwordConfirmation}
|
|
||||||
endAdornment={
|
|
||||||
<InputAdornment position='end'>
|
|
||||||
<IconButton
|
|
||||||
aria-label='password visibility'
|
|
||||||
size='large'
|
|
||||||
onClick={() => setShowPasswordConfirmation(!showPassword)}
|
|
||||||
onMouseDown={handleMouseDownPasswordConfirmation}
|
|
||||||
>
|
|
||||||
{showPasswordConfirmation ? <Visibility /> : <VisibilityOff />}
|
|
||||||
</IconButton>
|
|
||||||
</InputAdornment>
|
|
||||||
}
|
|
||||||
onChange={event => setPasswordConfirmation(event.target.value)}
|
|
||||||
onKeyUp={handleKeyUpPasswordConfirmation}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<Button
|
|
||||||
fullWidth
|
|
||||||
style={{marginTop: 20}}
|
|
||||||
variant='contained'
|
|
||||||
color='primary'
|
|
||||||
disabled={loading || password !== passwordConfirmation || password === ''}
|
|
||||||
onClick={handleClick}
|
|
||||||
>
|
|
||||||
<Typography style={{fontWeight: 'bold'}}>
|
|
||||||
Changer de mot de passe
|
|
||||||
</Typography>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
fullWidth
|
|
||||||
style={{marginTop: 20}}
|
|
||||||
variant='outlined'
|
|
||||||
color='primary'
|
|
||||||
onClick={() => router.push('/pwopose')}
|
|
||||||
>
|
|
||||||
<Typography style={{fontWeight: 'bold'}}>
|
|
||||||
Retour à la page de connexion
|
|
||||||
</Typography>
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{loading && <LinearProgress size={24} style={{width: '100%', marginTop: '1em'}} />}
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<Snackbar
|
|
||||||
open={open}
|
|
||||||
autoHideDuration={6000}
|
|
||||||
onClose={handleClose}
|
|
||||||
>
|
|
||||||
<Alert severity='error' onClose={handleClose}><strong>{error}</strong></Alert>
|
|
||||||
</Snackbar>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{passwordSuccess && (
|
|
||||||
<Snackbar
|
|
||||||
open={passwordSuccess}
|
|
||||||
onClose={handleClose}
|
|
||||||
>
|
|
||||||
<Alert severity='info' onClose={handleClose}>Votre changement de mot de passe a été pris en compte. Vous serez redirigé vers la page de connexion. <Link underline='hover' color='inherit' href='/pwopose'><strong>Cliquez ici si vous n’êtes pas redirigé vers la page de connexion</strong></Link></Alert>
|
|
||||||
</Snackbar>
|
|
||||||
)}
|
|
||||||
</Container>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
NewPassword.propTypes = {
|
|
||||||
code: PropTypes.string.isRequired
|
|
||||||
}
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
import {useState} from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import Button from '@mui/material/Button'
|
|
||||||
import TextField from '@mui/material/TextField'
|
|
||||||
import Dialog from '@mui/material/Dialog'
|
|
||||||
import DialogActions from '@mui/material/DialogActions'
|
|
||||||
import DialogContent from '@mui/material/DialogContent'
|
|
||||||
import DialogContentText from '@mui/material/DialogContentText'
|
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
|
||||||
|
|
||||||
import {validateEmail} from '../../lib/utils/emails'
|
|
||||||
import {jwennUserEpiEmail, passwordRequest} from '../../lib/oki-api'
|
|
||||||
|
|
||||||
export default function ResetDialog({lyen, title, activation, content, open, setOpen, setLoading, setError, setSuccess}) {
|
|
||||||
const [email, setEmail] = useState('')
|
|
||||||
|
|
||||||
const forgotPasswordRequest = async () => {
|
|
||||||
setLoading(true)
|
|
||||||
try {
|
|
||||||
await passwordRequest(lyen, email)
|
|
||||||
|
|
||||||
if (activation) {
|
|
||||||
const user = await jwennUserEpiEmail(email)
|
|
||||||
localStorage.setItem('user-id', user?.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(false)
|
|
||||||
setSuccess(true)
|
|
||||||
} catch {
|
|
||||||
setError('Une erreur s’est produite. Veuillez réessayer ultérieurement')
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetFrom = () => {
|
|
||||||
setEmail('')
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleClick = async () => {
|
|
||||||
forgotPasswordRequest()
|
|
||||||
setOpen(false)
|
|
||||||
resetFrom()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
setOpen(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Dialog open={open} onClose={handleClose}>
|
|
||||||
<DialogTitle>{title}</DialogTitle>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogContentText>
|
|
||||||
{content}
|
|
||||||
</DialogContentText>
|
|
||||||
<TextField
|
|
||||||
autoFocus
|
|
||||||
fullWidth
|
|
||||||
value={email}
|
|
||||||
margin='dense'
|
|
||||||
id='email'
|
|
||||||
label='Adresse e-mail'
|
|
||||||
type='email'
|
|
||||||
variant='standard'
|
|
||||||
onChange={event => setEmail(event.target.value)}
|
|
||||||
/>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button variant='contained' color='secondary' onClick={handleClose}>Annuler</Button>
|
|
||||||
<Button disabled={!validateEmail(email)} variant='contained' color='primary' onClick={handleClick}>Valider</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
ResetDialog.propTypes = {
|
|
||||||
lyen: PropTypes.string.isRequired,
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
activation: PropTypes.bool,
|
|
||||||
content: PropTypes.string.isRequired,
|
|
||||||
open: PropTypes.bool.isRequired,
|
|
||||||
setOpen: PropTypes.func.isRequired,
|
|
||||||
setLoading: PropTypes.func.isRequired,
|
|
||||||
setError: PropTypes.func.isRequired,
|
|
||||||
setSuccess: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
import {useState, forwardRef} from 'react'
|
|
||||||
import Button from '@mui/material/Button'
|
|
||||||
import Typography from '@mui/material/Typography'
|
|
||||||
import Box from '@mui/material/Box'
|
|
||||||
import Snackbar from '@mui/material/Snackbar'
|
|
||||||
import {LinearProgress} from '@mui/material'
|
|
||||||
import MuiAlert from '@mui/material/Alert'
|
|
||||||
import ResetDialog from './reset-dialog'
|
|
||||||
|
|
||||||
const Alert = forwardRef(function Alert(props, ref) {
|
|
||||||
return <MuiAlert ref={ref} elevation={6} variant='filled' {...props} />
|
|
||||||
})
|
|
||||||
|
|
||||||
export default function ResetPassword() {
|
|
||||||
const [open, setOpen] = useState(false)
|
|
||||||
const [error, setError] = useState(false)
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
const [success, setSuccess] = useState(false)
|
|
||||||
|
|
||||||
const handleClose = (event, reason) => {
|
|
||||||
if (reason === 'clickaway') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setError('')
|
|
||||||
setSuccess(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Box style={{marginTop: 20}}>
|
|
||||||
<Button
|
|
||||||
fullWidth
|
|
||||||
disabled={loading}
|
|
||||||
variant='outlined'
|
|
||||||
color='warning'
|
|
||||||
onClick={() => setOpen(true)}
|
|
||||||
>
|
|
||||||
<Typography style={{fontWeight: 'bold'}}>
|
|
||||||
Mot de passe oublié
|
|
||||||
</Typography>
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
{loading && <LinearProgress size={24} style={{width: '100%', marginTop: '1em'}} />}
|
|
||||||
{error && (
|
|
||||||
<Snackbar
|
|
||||||
open={Boolean(error)}
|
|
||||||
autoHideDuration={6000}
|
|
||||||
onClose={handleClose}
|
|
||||||
>
|
|
||||||
<Alert severity='error' onClose={handleClose}><strong>{error}</strong></Alert>
|
|
||||||
</Snackbar>
|
|
||||||
)}
|
|
||||||
{success && (
|
|
||||||
<Snackbar
|
|
||||||
open={success}
|
|
||||||
autoHideDuration={15_000}
|
|
||||||
onClose={handleClose}
|
|
||||||
>
|
|
||||||
<Alert severity='success' onClose={handleClose}><strong>Email envoyé avec succès. Vous pouvez changer votre mot de passe via le lien qui vous a été envoyé.</strong></Alert>
|
|
||||||
</Snackbar>
|
|
||||||
)}
|
|
||||||
<ResetDialog
|
|
||||||
lyen='forgot-password'
|
|
||||||
title='Mot de passe oublié'
|
|
||||||
content='Indiquez l’adresse e-mail utilisée lors de votre inscription. Vous receverez des instructions par e-mail.'
|
|
||||||
open={open}
|
|
||||||
setOpen={setOpen}
|
|
||||||
setLoading={setLoading}
|
|
||||||
setError={setError}
|
|
||||||
setSuccess={setSuccess}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import {useState, useEffect, forwardRef} from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import {useSession} from 'next-auth/react'
|
|
||||||
import MuiAlert from '@mui/material/Alert'
|
|
||||||
import Snackbar from '@mui/material/Snackbar'
|
|
||||||
import Box from '@mui/material/Box'
|
|
||||||
|
|
||||||
import Koneksyon from '../../components/sesyon/koneksyon'
|
|
||||||
import Dekoneksyon from '../../components/sesyon/dekoneksyon'
|
|
||||||
import EkriTeks from '../../components/soumet/ekri-teks'
|
|
||||||
import Footer from '../../components/footer'
|
|
||||||
|
|
||||||
import {jwennUserEpiToken, jwennUserEpiUsername} from '../../lib/oki-api'
|
|
||||||
import NewPassword from '../../components/password/new-password'
|
|
||||||
import ChwaTeks from '../../components/soumet/chwa-teks'
|
|
||||||
|
|
||||||
const Alert = forwardRef(function Alert(props, ref) {
|
|
||||||
return <MuiAlert ref={ref} elevation={6} variant='filled' {...props} />
|
|
||||||
})
|
|
||||||
|
|
||||||
export default function Pwopose({code}) {
|
|
||||||
const {data: session} = useSession()
|
|
||||||
const [localUsername, setLocalUsername] = useState(null)
|
|
||||||
const [username, setUsername] = useState(null)
|
|
||||||
const [open, setOpen] = useState(true)
|
|
||||||
const [selectedTeks, setSelectedTeks] = useState(null)
|
|
||||||
const [canAutoTranslate, setCanAutoTranslate] = useState(false)
|
|
||||||
|
|
||||||
const handleClose = (event, reason) => {
|
|
||||||
if (reason === 'clickaway') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setOpen(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (session?.jwt) {
|
|
||||||
const getUser = async token => {
|
|
||||||
const user = await jwennUserEpiToken(token)
|
|
||||||
setCanAutoTranslate(user.canAutoTranslate)
|
|
||||||
}
|
|
||||||
|
|
||||||
getUser(session.jwt)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (localStorage.getItem('username')) {
|
|
||||||
const username = localStorage.getItem('username')
|
|
||||||
setLocalUsername(username)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (localUsername) {
|
|
||||||
const getUser = async username => {
|
|
||||||
const user = await jwennUserEpiUsername(username)
|
|
||||||
setUsername(user?.username)
|
|
||||||
}
|
|
||||||
|
|
||||||
getUser(localUsername)
|
|
||||||
}
|
|
||||||
}, [localUsername])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (username && localStorage.getItem('username')) {
|
|
||||||
localStorage.removeItem('username')
|
|
||||||
}
|
|
||||||
}, [username])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box sx={{display: 'flex', flexDirection: 'column', minHeight: '100vh'}}>
|
|
||||||
<Box sx={{flexGrow: 1, marginTop: 1, marginBottom: 10}}>
|
|
||||||
{!session && !code && (
|
|
||||||
<Koneksyon
|
|
||||||
chimen='/pwopose'
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!session && code && (
|
|
||||||
<NewPassword code={code} />
|
|
||||||
)}
|
|
||||||
{session && session.user && (
|
|
||||||
<>
|
|
||||||
<Dekoneksyon position='absolute' top={95} left={5} chimen='/pwopose' />
|
|
||||||
<ChwaTeks selectedTeks={selectedTeks} setSelectedTeks={setSelectedTeks} />
|
|
||||||
<EkriTeks canAutoTranslate={canAutoTranslate} selectedTeks={selectedTeks} setSelectedTeks={setSelectedTeks} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{session && !session.user && (
|
|
||||||
<Dekoneksyon position='absolute' top={95} left={5} chimen='/pwopose' />
|
|
||||||
)}
|
|
||||||
{username && (
|
|
||||||
<Snackbar
|
|
||||||
open={open}
|
|
||||||
autoHideDuration={10_000}
|
|
||||||
onClose={handleClose}
|
|
||||||
>
|
|
||||||
<Alert severity='success' onClose={handleClose}><strong>Bonjour {username}, votre compte a été activé avec succès. Vous pouvez vous connecter.</strong></Alert>
|
|
||||||
</Snackbar>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
<Footer />
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Pwopose.propTypes = {
|
|
||||||
code: PropTypes.string
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import {useState} from 'react'
|
|
||||||
import {styled} from '@mui/material/styles'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import {signOut} from 'next-auth/react'
|
|
||||||
import {Tooltip, Fab, Zoom} from '@mui/material'
|
|
||||||
import ExitToAppIcon from '@mui/icons-material/ExitToApp'
|
|
||||||
|
|
||||||
import Alet from '../alet'
|
|
||||||
|
|
||||||
const PREFIX = 'Dekoneksyon'
|
|
||||||
|
|
||||||
const classes = {
|
|
||||||
tooltip: `${PREFIX}-tooltip`,
|
|
||||||
dekoneksyon: `${PREFIX}-dekoneksyon`
|
|
||||||
}
|
|
||||||
|
|
||||||
const Root = styled('div')(() => ({
|
|
||||||
[`& .${classes.dekoneksyon}`]: {
|
|
||||||
position: props => props.position,
|
|
||||||
top: props => props.top,
|
|
||||||
left: props => props.left,
|
|
||||||
zIndex: 0
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
const DekoneksonTooltip = Tooltip
|
|
||||||
|
|
||||||
function Dekoneksyon({tooltipPlacement}) {
|
|
||||||
const [esOuve, meteEsOuve] = useState(false)
|
|
||||||
|
|
||||||
return (
|
|
||||||
(
|
|
||||||
<Root>
|
|
||||||
<DekoneksonTooltip
|
|
||||||
title='Se déconnecter'
|
|
||||||
placement={tooltipPlacement}
|
|
||||||
TransitionComponent={Zoom}
|
|
||||||
classes={{
|
|
||||||
tooltip: classes.tooltip
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Fab
|
|
||||||
className={classes.dekoneksyon}
|
|
||||||
color='error'
|
|
||||||
aria-label='logout'
|
|
||||||
size='small'
|
|
||||||
onClick={() => meteEsOuve(true)}
|
|
||||||
>
|
|
||||||
<ExitToAppIcon />
|
|
||||||
</Fab>
|
|
||||||
</DekoneksonTooltip>
|
|
||||||
<Alet esOuve={esOuve} meteEsOuve={meteEsOuve} handleKonfime={() => signOut()} />
|
|
||||||
</Root>
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Dekoneksyon.propTypes = {
|
|
||||||
tooltipPlacement: PropTypes.string
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Dekoneksyon
|
|
||||||
@@ -1,459 +0,0 @@
|
|||||||
import {useEffect, useState, forwardRef} from 'react'
|
|
||||||
import {signIn} from 'next-auth/react'
|
|
||||||
import {useRouter} from 'next/navigation'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import Box from '@mui/material/Box'
|
|
||||||
import Button from '@mui/material/Button'
|
|
||||||
import Container from '@mui/material/Container'
|
|
||||||
import FormControl from '@mui/material/FormControl'
|
|
||||||
import IconButton from '@mui/material/IconButton'
|
|
||||||
import Input from '@mui/material/Input'
|
|
||||||
import InputAdornment from '@mui/material/InputAdornment'
|
|
||||||
import InputLabel from '@mui/material/InputLabel'
|
|
||||||
import LinearProgress from '@mui/material/LinearProgress'
|
|
||||||
import Snackbar from '@mui/material/Snackbar'
|
|
||||||
import Tab from '@mui/material/Tab'
|
|
||||||
import Tabs from '@mui/material/Tabs'
|
|
||||||
import Typography from '@mui/material/Typography'
|
|
||||||
import Grid from '@mui/material/Grid'
|
|
||||||
import Visibility from '@mui/icons-material/Visibility'
|
|
||||||
import VisibilityOff from '@mui/icons-material/VisibilityOff'
|
|
||||||
import MuiAlert from '@mui/material/Alert'
|
|
||||||
import LoginIcon from '@mui/icons-material/Login'
|
|
||||||
import AppRegistrationRoundedIcon from '@mui/icons-material/AppRegistrationRounded'
|
|
||||||
import axios from 'axios'
|
|
||||||
|
|
||||||
import {validateEmail} from '../../lib/utils/emails'
|
|
||||||
import ResetPassword from '../password/reset-password'
|
|
||||||
import ResetDialog from '../password/reset-dialog'
|
|
||||||
import LoginProvider from './login-provider'
|
|
||||||
|
|
||||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3001'
|
|
||||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:1337'
|
|
||||||
|
|
||||||
const PROVIDERS = [
|
|
||||||
{
|
|
||||||
id: 'google',
|
|
||||||
title: 'Google',
|
|
||||||
width: 16,
|
|
||||||
height: 16
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'twitter',
|
|
||||||
title: 'Twitter',
|
|
||||||
width: 16,
|
|
||||||
height: 13
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
function TabPanel(props) {
|
|
||||||
const {children, value, index, ...other} = props
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
role='tabpanel'
|
|
||||||
hidden={value !== index}
|
|
||||||
id={`simple-tabpanel-${index}`}
|
|
||||||
aria-labelledby={`simple-tab-${index}`}
|
|
||||||
{...other}
|
|
||||||
>
|
|
||||||
{value === index && (
|
|
||||||
<Box sx={{p: 3}}>
|
|
||||||
{children}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
TabPanel.propTypes = {
|
|
||||||
children: PropTypes.node,
|
|
||||||
index: PropTypes.number.isRequired,
|
|
||||||
value: PropTypes.number.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
function a11yProps(index) {
|
|
||||||
return {
|
|
||||||
id: `simple-tab-${index}`,
|
|
||||||
'aria-controls': `simple-tabpanel-${index}`,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const Alert = forwardRef(function Alert(props, ref) {
|
|
||||||
return <MuiAlert ref={ref} elevation={6} variant='filled' {...props} />
|
|
||||||
})
|
|
||||||
|
|
||||||
function Koneksyon({chimen}) {
|
|
||||||
const [error, setError] = useState('')
|
|
||||||
const [loginCredentials, setLoginCredentials] = useState({username: '', password: ''})
|
|
||||||
const [registerCredentials, setRegisterCredentials] = useState({username: '', email: '', password: ''})
|
|
||||||
const [passwordVerification, setPasswordVerification] = useState('')
|
|
||||||
const [showPassword, setShowPassword] = useState(false)
|
|
||||||
const [showRegisterPassword, setShowRegisterPassword] = useState(false)
|
|
||||||
const [showVerificationPassword, setShowVerificationPassword] = useState(false)
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
const [open, setOpen] = useState(true)
|
|
||||||
const [success, setSuccess] = useState(false)
|
|
||||||
const [openDialog, setOpenDialog] = useState(false)
|
|
||||||
const [value, setValue] = useState(0)
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const handleUpdate = update => {
|
|
||||||
setLoginCredentials({...loginCredentials, ...update})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleRegisterUpdate = update => {
|
|
||||||
setRegisterCredentials({...registerCredentials, ...update})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleChange = (event, newValue) => {
|
|
||||||
setValue(newValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
const register = async () => {
|
|
||||||
try {
|
|
||||||
const response = await axios.post(`${apiUrl}/auth/local/register`, {
|
|
||||||
...registerCredentials
|
|
||||||
}, {
|
|
||||||
headers: {
|
|
||||||
'content-type': 'application/json'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
localStorage.setItem('username', `${response?.data?.user?.username}`)
|
|
||||||
setSuccess(true)
|
|
||||||
resetRegisterForm()
|
|
||||||
} catch (error_) {
|
|
||||||
if (error_.message.endsWith(400)) {
|
|
||||||
setError('E-mail ou utilisateur déjà enregistré')
|
|
||||||
} else {
|
|
||||||
setError('Une erreur s’est produite')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetRegisterForm = () => {
|
|
||||||
setRegisterCredentials({username: '', email: '', password: ''})
|
|
||||||
setPasswordVerification('')
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleClickLogin = async () => {
|
|
||||||
if (!validateEmail(loginCredentials.username) || loginCredentials.password === '') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true)
|
|
||||||
const response = await signIn('credentials', {
|
|
||||||
callbackUrl: `${siteUrl}${chimen}`,
|
|
||||||
redirect: false,
|
|
||||||
...loginCredentials
|
|
||||||
})
|
|
||||||
if (response.error) {
|
|
||||||
setError(response.error)
|
|
||||||
setLoading(false)
|
|
||||||
} else if (response.ok) {
|
|
||||||
setLoading(false)
|
|
||||||
router.push(chimen)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleClickRegister = async () => {
|
|
||||||
setLoading(true)
|
|
||||||
if (!validateEmail(registerCredentials.email) || registerCredentials.password === '') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (registerCredentials.password !== passwordVerification) {
|
|
||||||
setError('Les 2 mots de passe de correspondent pas')
|
|
||||||
setLoading(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (registerCredentials.username.length < 3) {
|
|
||||||
setError('Le nom d’utilisateur est trop court, 3 caratères minimum')
|
|
||||||
setLoading(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (registerCredentials.password.length < 6) {
|
|
||||||
setError('Le mot de passe est trop court, 6 caratères minimum')
|
|
||||||
setLoading(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await register()
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleClose = (event, reason) => {
|
|
||||||
if (reason === 'clickaway') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setOpen(false)
|
|
||||||
setSuccess(false)
|
|
||||||
setError('')
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (error) {
|
|
||||||
setOpen(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}, [error])
|
|
||||||
|
|
||||||
const handleClickShowPassword = () => {
|
|
||||||
setShowPassword(!showPassword)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleClickShowRegisterPassword = () => {
|
|
||||||
setShowRegisterPassword(!showRegisterPassword)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleClickShowVerificationPassword = () => {
|
|
||||||
setShowVerificationPassword(!showVerificationPassword)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleMouseDownPassword = event => {
|
|
||||||
event.preventDefault()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleMouseDownRegisterPassword = event => {
|
|
||||||
event.preventDefault()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleMouseDownVerificationPassword = event => {
|
|
||||||
event.preventDefault()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleKeyUpLogin = event => {
|
|
||||||
if (event.keyCode === 13) {
|
|
||||||
handleClickLogin()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleKeyUpRegister = event => {
|
|
||||||
if (event.keyCode === 13) {
|
|
||||||
handleClickRegister()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container sx={{marginTop: 6}} maxWidth='sm'>
|
|
||||||
|
|
||||||
<Box sx={{width: '100%'}}>
|
|
||||||
<Tabs centered value={value} aria-label='basic tabs example' onChange={handleChange}>
|
|
||||||
<Tab icon={<LoginIcon />} label='Se connecter' {...a11yProps(0)} />
|
|
||||||
<Tab icon={<AppRegistrationRoundedIcon />} label='S’inscrire' {...a11yProps(1)} />
|
|
||||||
</Tabs>
|
|
||||||
<TabPanel value={value} index={0}>
|
|
||||||
|
|
||||||
<Box sx={{textAlign: 'center', marginBottom: 3}}>
|
|
||||||
<Typography gutterBottom textalign='center'>Connectez-vous avec</Typography>
|
|
||||||
<Grid sx={{justifyContent: 'center'}} container columnSpacing={{xs: 1, sm: 2, md: 3}}>
|
|
||||||
{PROVIDERS.map(({id, title, width, height}) => (
|
|
||||||
<Grid sx={{marginTop: 1}} key={id}>
|
|
||||||
<LoginProvider id={id} title={title} width={width} height={height} callbackUrl={`${siteUrl}${chimen}`} />
|
|
||||||
</Grid>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box sx={{textAlign: 'center', marginBottom: 3}}>
|
|
||||||
<Typography>ou utilisez votre e-mail pour vous identifier</Typography>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<FormControl fullWidth autoComplete='off'>
|
|
||||||
<InputLabel htmlFor='username'>E-mail</InputLabel>
|
|
||||||
<Input
|
|
||||||
autoComplete='new-password'
|
|
||||||
value={loginCredentials.username}
|
|
||||||
name='username'
|
|
||||||
type='email'
|
|
||||||
id='email'
|
|
||||||
onChange={event => handleUpdate({username: event.target.value})}
|
|
||||||
onKeyUp={handleKeyUpLogin}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormControl fullWidth style={{marginTop: '1em'}}>
|
|
||||||
<InputLabel htmlFor='password'>Mot de passe</InputLabel>
|
|
||||||
<Input
|
|
||||||
autoComplete='new-password'
|
|
||||||
value={loginCredentials.password}
|
|
||||||
name='password'
|
|
||||||
type={showPassword ? 'text' : 'password'}
|
|
||||||
id='password'
|
|
||||||
endAdornment={
|
|
||||||
<InputAdornment position='end'>
|
|
||||||
<IconButton
|
|
||||||
aria-label='password visibility'
|
|
||||||
size='large'
|
|
||||||
onClick={handleClickShowPassword}
|
|
||||||
onMouseDown={handleMouseDownPassword}
|
|
||||||
>
|
|
||||||
{showPassword ? <Visibility /> : <VisibilityOff />}
|
|
||||||
</IconButton>
|
|
||||||
</InputAdornment>
|
|
||||||
}
|
|
||||||
onChange={event => handleUpdate({password: event.target.value})}
|
|
||||||
onKeyUp={handleKeyUpLogin}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
fullWidth
|
|
||||||
disabled={loading || !validateEmail(loginCredentials.username) || loginCredentials.password === ''}
|
|
||||||
style={{marginTop: 20}}
|
|
||||||
variant='contained'
|
|
||||||
color='primary'
|
|
||||||
onClick={handleClickLogin}
|
|
||||||
>
|
|
||||||
<Typography style={{fontWeight: 'bold'}}>
|
|
||||||
Connexion
|
|
||||||
</Typography>
|
|
||||||
</Button>
|
|
||||||
<ResetPassword />
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel value={value} index={1}>
|
|
||||||
<FormControl fullWidth>
|
|
||||||
<InputLabel htmlFor='register-email'>E-mail</InputLabel>
|
|
||||||
<Input
|
|
||||||
autoComplete='email'
|
|
||||||
value={registerCredentials.email}
|
|
||||||
name='register-email'
|
|
||||||
type='email'
|
|
||||||
id='register-email'
|
|
||||||
onChange={event => handleRegisterUpdate({email: event.target.value})}
|
|
||||||
onKeyUp={handleKeyUpRegister}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormControl fullWidth style={{marginTop: '1em'}}>
|
|
||||||
<InputLabel htmlFor='register-username'>Nom d’utilisateur</InputLabel>
|
|
||||||
<Input
|
|
||||||
autoComplete='username'
|
|
||||||
value={registerCredentials.username}
|
|
||||||
name='register-username'
|
|
||||||
type='text'
|
|
||||||
id='register-username'
|
|
||||||
onChange={event => handleRegisterUpdate({username: event.target.value})}
|
|
||||||
onKeyUp={handleKeyUpRegister}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormControl fullWidth style={{marginTop: '1em'}}>
|
|
||||||
<InputLabel htmlFor='password'>Mot de passe</InputLabel>
|
|
||||||
<Input
|
|
||||||
autoComplete='new-password' // Disable auto complete
|
|
||||||
value={registerCredentials.password}
|
|
||||||
name='register-password'
|
|
||||||
type={showRegisterPassword ? 'text' : 'password'}
|
|
||||||
id='register-password'
|
|
||||||
endAdornment={
|
|
||||||
<InputAdornment position='end'>
|
|
||||||
<IconButton
|
|
||||||
aria-label='password visibility'
|
|
||||||
size='large'
|
|
||||||
onClick={handleClickShowRegisterPassword}
|
|
||||||
onMouseDown={handleMouseDownRegisterPassword}
|
|
||||||
>
|
|
||||||
{showRegisterPassword ? <Visibility /> : <VisibilityOff />}
|
|
||||||
</IconButton>
|
|
||||||
</InputAdornment>
|
|
||||||
}
|
|
||||||
onChange={event => handleRegisterUpdate({password: event.target.value})}
|
|
||||||
onKeyUp={handleKeyUpRegister}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormControl fullWidth style={{marginTop: '1em'}}>
|
|
||||||
<InputLabel htmlFor='password'>Vérification du mot de passe</InputLabel>
|
|
||||||
<Input
|
|
||||||
value={passwordVerification}
|
|
||||||
name='verification-password'
|
|
||||||
type={showVerificationPassword ? 'text' : 'password'}
|
|
||||||
id='verification-password'
|
|
||||||
error={registerCredentials.password !== passwordVerification}
|
|
||||||
endAdornment={
|
|
||||||
<InputAdornment position='end'>
|
|
||||||
<IconButton
|
|
||||||
aria-label='password visibility'
|
|
||||||
size='large'
|
|
||||||
onClick={handleClickShowVerificationPassword}
|
|
||||||
onMouseDown={handleMouseDownVerificationPassword}
|
|
||||||
>
|
|
||||||
{showVerificationPassword ? <Visibility /> : <VisibilityOff />}
|
|
||||||
</IconButton>
|
|
||||||
</InputAdornment>
|
|
||||||
}
|
|
||||||
onChange={event => setPasswordVerification(event.target.value)}
|
|
||||||
onKeyUp={handleKeyUpRegister}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<Button
|
|
||||||
fullWidth
|
|
||||||
disabled={loading || !validateEmail(registerCredentials.email) || registerCredentials.username === '' || registerCredentials.password === ''}
|
|
||||||
style={{marginTop: 20}}
|
|
||||||
variant='contained'
|
|
||||||
color='primary'
|
|
||||||
onClick={handleClickRegister}
|
|
||||||
>
|
|
||||||
<Typography style={{fontWeight: 'bold'}}>
|
|
||||||
Inscription
|
|
||||||
</Typography>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
fullWidth
|
|
||||||
style={{marginTop: 20}}
|
|
||||||
variant='outlined'
|
|
||||||
color='warning'
|
|
||||||
onClick={() => setOpenDialog(true)}
|
|
||||||
>
|
|
||||||
<Typography style={{fontWeight: 'bold'}}>
|
|
||||||
Renvoyer l’e-mail de confirmation
|
|
||||||
</Typography>
|
|
||||||
</Button>
|
|
||||||
</TabPanel>
|
|
||||||
</Box>
|
|
||||||
<ResetDialog
|
|
||||||
activation
|
|
||||||
lyen='send-email-confirmation'
|
|
||||||
title='Envoi de l’e-mail de confirmation'
|
|
||||||
content='Si vous n’avez pas reçu l’e-mail de confirmation, renseignez le champ et validez le formulaire.'
|
|
||||||
open={openDialog}
|
|
||||||
setOpen={setOpenDialog}
|
|
||||||
setLoading={setLoading}
|
|
||||||
setError={setError}
|
|
||||||
setSuccess={setSuccess}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{loading && <LinearProgress size={24} style={{width: '100%'}} />}
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<Snackbar
|
|
||||||
open={open}
|
|
||||||
autoHideDuration={6000}
|
|
||||||
onClose={handleClose}
|
|
||||||
>
|
|
||||||
<Alert severity='error' onClose={handleClose}><strong>{error}</strong></Alert>
|
|
||||||
</Snackbar>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{success && (
|
|
||||||
<Snackbar
|
|
||||||
open={success}
|
|
||||||
autoHideDuration={15_000}
|
|
||||||
onClose={handleClose}
|
|
||||||
>
|
|
||||||
<Alert severity='success' onClose={handleClose}><strong>Veuillez confirmer votre adresse e-mail via le lien qui vous a été envoyé.</strong></Alert>
|
|
||||||
</Snackbar>
|
|
||||||
)}
|
|
||||||
</Container>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Koneksyon.propTypes = {
|
|
||||||
chimen: PropTypes.string.isRequired
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Koneksyon
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import PropTypes from 'prop-types'
|
|
||||||
import Link from 'next/link'
|
|
||||||
import Image from 'next/image'
|
|
||||||
import {signIn} from 'next-auth/react'
|
|
||||||
import Button from '@mui/material/Button'
|
|
||||||
|
|
||||||
export default function LoginProvider({id, title, width, height, callbackUrl}) {
|
|
||||||
const hanleClick = event => {
|
|
||||||
event.preventDefault()
|
|
||||||
signIn(id, {
|
|
||||||
callbackUrl
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link passHref href='/api/auth/signin'>
|
|
||||||
<Button variant='outlined' color='info' startIcon={
|
|
||||||
<Image
|
|
||||||
width={width}
|
|
||||||
height={height}
|
|
||||||
alt={title}
|
|
||||||
src={`/images/${id === 'twitter' ? 'x' : id}.svg`}
|
|
||||||
/>
|
|
||||||
} onClick={hanleClick}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
LoginProvider.propTypes = {
|
|
||||||
id: PropTypes.string.isRequired,
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
width: PropTypes.number.isRequired,
|
|
||||||
height: PropTypes.number.isRequired,
|
|
||||||
callbackUrl: PropTypes.string.isRequired
|
|
||||||
}
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
import {useState} from 'react'
|
|
||||||
import {styled} from '@mui/material/styles'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import {MenuItem, Menu, Button} from '@mui/material'
|
|
||||||
import {uniq} from 'lodash'
|
|
||||||
|
|
||||||
import OtomatikSwitch from './otomatik-switch'
|
|
||||||
|
|
||||||
const PREFIX = 'AjouteTradiksyon'
|
|
||||||
|
|
||||||
const classes = {
|
|
||||||
paper: `${PREFIX}-paper`,
|
|
||||||
root: `${PREFIX}-root`
|
|
||||||
}
|
|
||||||
|
|
||||||
const Root = styled('div')((
|
|
||||||
{
|
|
||||||
theme
|
|
||||||
}
|
|
||||||
) => ({
|
|
||||||
[`& .${classes.paper}`]: {
|
|
||||||
border: '1px solid #d3d4d5'
|
|
||||||
},
|
|
||||||
|
|
||||||
[`& .${classes.root}`]: {
|
|
||||||
'&:focus': {
|
|
||||||
backgroundColor: theme.palette.primary.main,
|
|
||||||
'& .MuiListItemIcon-root, & .MuiListItemText-primary': {
|
|
||||||
color: theme.palette.common.white
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
function StyledMenu(props) {
|
|
||||||
return (
|
|
||||||
<Menu
|
|
||||||
elevation={0}
|
|
||||||
anchorOrigin={{
|
|
||||||
vertical: 'top',
|
|
||||||
horizontal: 'center'
|
|
||||||
}}
|
|
||||||
transformOrigin={{
|
|
||||||
vertical: 'bottom',
|
|
||||||
horizontal: 'center'
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const StyledMenuItem = MenuItem
|
|
||||||
|
|
||||||
function AjouteTradiksyon({showSwitch, disableSwitch, tradiksyonOtomatik, setTradiksyonOtomatik, chwaLang, setChwaLang}) {
|
|
||||||
const [anchorElement, setAnchorElement] = useState(null)
|
|
||||||
|
|
||||||
const handleClick = event => {
|
|
||||||
setAnchorElement(event.currentTarget)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleClose = event => {
|
|
||||||
setAnchorElement(null)
|
|
||||||
const chwaInik = uniq([...chwaLang, event.currentTarget.id])
|
|
||||||
setChwaLang(chwaInik.filter(c => c !== ''))
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{showSwitch && (
|
|
||||||
<OtomatikSwitch tradiksyonOtomatik={tradiksyonOtomatik} setTradiksyonOtomatik={setTradiksyonOtomatik} disabled={disableSwitch} />
|
|
||||||
)}
|
|
||||||
<Root style={{textAlign: 'center', marginTop: 20}}>
|
|
||||||
<Button
|
|
||||||
aria-controls='lang-menu'
|
|
||||||
variant='outlined'
|
|
||||||
aria-haspopup='true'
|
|
||||||
onClick={handleClick}
|
|
||||||
>
|
|
||||||
Ajouter une traduction <br /> 🇺🇸 🇫🇷 🇪🇸 🇩🇪 🇮🇹
|
|
||||||
</Button>
|
|
||||||
<StyledMenu
|
|
||||||
keepMounted
|
|
||||||
id='lang-menu'
|
|
||||||
anchorEl={anchorElement}
|
|
||||||
open={Boolean(anchorElement)}
|
|
||||||
classes={{
|
|
||||||
paper: classes.paper
|
|
||||||
}}
|
|
||||||
onClose={handleClose}
|
|
||||||
>
|
|
||||||
<StyledMenuItem
|
|
||||||
id='en'
|
|
||||||
classes={{
|
|
||||||
root: classes.root
|
|
||||||
}}
|
|
||||||
onClick={handleClose}
|
|
||||||
>🇺🇸 Anglais (US)</StyledMenuItem>
|
|
||||||
<StyledMenuItem
|
|
||||||
id='fr'
|
|
||||||
classes={{
|
|
||||||
root: classes.root
|
|
||||||
}}
|
|
||||||
onClick={handleClose}
|
|
||||||
>🇫🇷 Français</StyledMenuItem>
|
|
||||||
<StyledMenuItem
|
|
||||||
id='es'
|
|
||||||
classes={{
|
|
||||||
root: classes.root
|
|
||||||
}}
|
|
||||||
onClick={handleClose}
|
|
||||||
>🇪🇸 Espagnol</StyledMenuItem>
|
|
||||||
<StyledMenuItem
|
|
||||||
id='de'
|
|
||||||
classes={{
|
|
||||||
root: classes.root
|
|
||||||
}}
|
|
||||||
onClick={handleClose}
|
|
||||||
>🇩🇪 Allemand</StyledMenuItem>
|
|
||||||
<StyledMenuItem
|
|
||||||
id='it'
|
|
||||||
classes={{
|
|
||||||
root: classes.root
|
|
||||||
}}
|
|
||||||
onClick={handleClose}
|
|
||||||
>🇮🇹 Italien</StyledMenuItem>
|
|
||||||
</StyledMenu>
|
|
||||||
</Root>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
AjouteTradiksyon.propTypes = {
|
|
||||||
showSwitch: PropTypes.bool.isRequired,
|
|
||||||
disableSwitch: PropTypes.bool.isRequired,
|
|
||||||
tradiksyonOtomatik: PropTypes.bool.isRequired,
|
|
||||||
setTradiksyonOtomatik: PropTypes.func.isRequired,
|
|
||||||
chwaLang: PropTypes.array.isRequired,
|
|
||||||
setChwaLang: PropTypes.func.isRequired
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AjouteTradiksyon
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
import {useState} from 'react'
|
|
||||||
import {useSession} from 'next-auth/react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import Button from '@mui/material/Button'
|
|
||||||
import Avatar from '@mui/material/Avatar'
|
|
||||||
import List from '@mui/material/List'
|
|
||||||
import ListItem from '@mui/material/ListItem'
|
|
||||||
import ListItemAvatar from '@mui/material/ListItemAvatar'
|
|
||||||
import ListItemText from '@mui/material/ListItemText'
|
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
|
||||||
import Dialog from '@mui/material/Dialog'
|
|
||||||
import {green} from '@mui/material/colors'
|
|
||||||
import Container from '@mui/material/Container'
|
|
||||||
import CircularProgress from '@mui/material/CircularProgress'
|
|
||||||
import Typography from '@mui/material/Typography'
|
|
||||||
import AudiotrackIcon from '@mui/icons-material/Audiotrack'
|
|
||||||
|
|
||||||
import {jwennUserEpiToken} from '../../lib/oki-api'
|
|
||||||
|
|
||||||
function Chwa(props) {
|
|
||||||
const {onClose, teksLis, selectedTeks, loading, open} = props
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
onClose(selectedTeks)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleListItemClick = value => {
|
|
||||||
onClose(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={open} onClose={handleClose}>
|
|
||||||
<DialogTitle>Choisir la parole à modifier</DialogTitle>
|
|
||||||
{loading ? (
|
|
||||||
<Container sx={{textAlign: 'center', marginBottom: 1}}>
|
|
||||||
<CircularProgress size={20} sx={{color: '#29d'}} />
|
|
||||||
</Container>
|
|
||||||
) : (
|
|
||||||
<List sx={{pt: 0}}>
|
|
||||||
{teksLis.length > 0 ? teksLis.map(({id, titre, artistes}) => (
|
|
||||||
<ListItem key={id} button onClick={() => handleListItemClick(id)}>
|
|
||||||
<ListItemAvatar>
|
|
||||||
<Avatar sx={{bgcolor: green[100], color: green[600]}}>
|
|
||||||
<AudiotrackIcon />
|
|
||||||
</Avatar>
|
|
||||||
</ListItemAvatar>
|
|
||||||
<ListItemText primary={`${new Intl.ListFormat('fr').format(artistes.map(({alias}) => alias))} - ${titre}`} />
|
|
||||||
</ListItem>
|
|
||||||
)) : (
|
|
||||||
<Typography sx={{textAlign: 'center'}} variant='button' component='div'>Aucun texte</Typography>
|
|
||||||
)}
|
|
||||||
</List>
|
|
||||||
)}
|
|
||||||
</Dialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Chwa.propTypes = {
|
|
||||||
onClose: PropTypes.func.isRequired,
|
|
||||||
teksLis: PropTypes.array.isRequired,
|
|
||||||
loading: PropTypes.bool.isRequired,
|
|
||||||
open: PropTypes.bool.isRequired,
|
|
||||||
selectedTeks: PropTypes.object,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ChwaTeks({selectedTeks, setSelectedTeks}) {
|
|
||||||
const {data: session} = useSession()
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
const [open, setOpen] = useState(false)
|
|
||||||
const [teksLis, setTeksLis] = useState([])
|
|
||||||
|
|
||||||
const handleClickOpen = async () => {
|
|
||||||
setOpen(true)
|
|
||||||
setLoading(true)
|
|
||||||
|
|
||||||
const user = await jwennUserEpiToken(session?.jwt)
|
|
||||||
const {paroles} = user
|
|
||||||
const parolesList = paroles && paroles.length > 0 ? paroles : []
|
|
||||||
|
|
||||||
setTeksLis(parolesList)
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleClose = value => {
|
|
||||||
const teks = teksLis.find(({id}) => id === value)
|
|
||||||
setSelectedTeks(teks)
|
|
||||||
setOpen(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container sx={{textAlign: 'center', marginBottom: 1}} maxWidth='sm'>
|
|
||||||
<Button variant='outlined' onClick={handleClickOpen}>
|
|
||||||
Modifier mes paroles
|
|
||||||
</Button>
|
|
||||||
<Chwa
|
|
||||||
selectedTeks={selectedTeks}
|
|
||||||
teksLis={teksLis}
|
|
||||||
open={open}
|
|
||||||
loading={loading}
|
|
||||||
onClose={handleClose}
|
|
||||||
/>
|
|
||||||
</Container>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
ChwaTeks.propTypes = {
|
|
||||||
selectedTeks: PropTypes.object,
|
|
||||||
setSelectedTeks: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
@@ -1,433 +0,0 @@
|
|||||||
import {useCallback, useEffect, forwardRef, useState} from 'react'
|
|
||||||
import {styled} from '@mui/material/styles'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import axios from 'axios'
|
|
||||||
import {useSession} from 'next-auth/react'
|
|
||||||
import Box from '@mui/material/Box'
|
|
||||||
import Button from '@mui/material/Button'
|
|
||||||
import Container from '@mui/material/Container'
|
|
||||||
import FormControl from '@mui/material/FormControl'
|
|
||||||
import FormHelperText from '@mui/material/FormHelperText'
|
|
||||||
import Grid from '@mui/material/Grid'
|
|
||||||
import IconButton from '@mui/material/IconButton'
|
|
||||||
import InputAdornment from '@mui/material/InputAdornment'
|
|
||||||
import InputLabel from '@mui/material/InputLabel'
|
|
||||||
import LinearProgress from '@mui/material/LinearProgress'
|
|
||||||
import OutlinedInput from '@mui/material/OutlinedInput'
|
|
||||||
import Snackbar from '@mui/material/Snackbar'
|
|
||||||
import TextField from '@mui/material/TextField'
|
|
||||||
import Tooltip from '@mui/material/Tooltip'
|
|
||||||
import Typography from '@mui/material/Typography'
|
|
||||||
|
|
||||||
import MuiAlert from '@mui/material/Alert'
|
|
||||||
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'
|
|
||||||
|
|
||||||
import {jwennAnTeks} from '../../lib/oki-api'
|
|
||||||
|
|
||||||
import AjouteTradiksyon from './ajoute-tradiksyon'
|
|
||||||
|
|
||||||
const PREFIX = 'EkriTeks'
|
|
||||||
|
|
||||||
const classes = {
|
|
||||||
tooltip: `${PREFIX}-tooltip`
|
|
||||||
}
|
|
||||||
|
|
||||||
const StyledContainer = styled(Container)(() => ({
|
|
||||||
[`& .${classes.tooltip}`]: {
|
|
||||||
fontSize: 18
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:1337'
|
|
||||||
|
|
||||||
const textLabels = {
|
|
||||||
fr: {
|
|
||||||
title: 'Traduction',
|
|
||||||
help: 'Traduction des paroles',
|
|
||||||
emoji: '🇫🇷'
|
|
||||||
},
|
|
||||||
en: {
|
|
||||||
title: 'Translation',
|
|
||||||
help: 'Translation of the lyrics',
|
|
||||||
emoji: '🇬🇧'
|
|
||||||
},
|
|
||||||
es: {
|
|
||||||
title: 'Traducción',
|
|
||||||
help: 'Traducción de la letra',
|
|
||||||
emoji: '🇪🇸'
|
|
||||||
},
|
|
||||||
de: {
|
|
||||||
title: 'Übersetzung',
|
|
||||||
help: 'Übersetzung von Liedtexten',
|
|
||||||
emoji: '🇩🇪'
|
|
||||||
},
|
|
||||||
it: {
|
|
||||||
title: 'Traduzione',
|
|
||||||
help: 'Traduzione del testo',
|
|
||||||
emoji: '🇮🇹'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const Alert = forwardRef(function Alert(props, ref) {
|
|
||||||
return <MuiAlert ref={ref} elevation={6} variant='filled' {...props} />
|
|
||||||
})
|
|
||||||
|
|
||||||
const RemoveTooltip = Tooltip
|
|
||||||
|
|
||||||
function EkriTeks({canAutoTranslate, selectedTeks, setSelectedTeks}) {
|
|
||||||
const {data: session} = useSession()
|
|
||||||
const {jwt, user} = session
|
|
||||||
const [teksEkri, setTeksEkri] = useState({awtis: '', tit: '', transkripsyon: ''})
|
|
||||||
const [tradiksyon, setTradiksyon] = useState({fr: '', en: '', es: '', de: '', it: ''})
|
|
||||||
const [kiChawLang, setKiChwalang] = useState(['fr'])
|
|
||||||
const [error, setError] = useState('')
|
|
||||||
const [success, setSuccess] = useState('')
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
const [currentTeksId, setCurrentTeksId] = useState(null)
|
|
||||||
const [open, setOpen] = useState(false)
|
|
||||||
const [tradiksyonOtomatik, setTradiksyonOtomatik] = useState(false)
|
|
||||||
|
|
||||||
const handleUpdate = useCallback(update => {
|
|
||||||
setTeksEkri({...teksEkri, ...update})
|
|
||||||
}, [teksEkri])
|
|
||||||
|
|
||||||
const handleUpdateTradiksyon = useCallback(update => {
|
|
||||||
setTradiksyon({...tradiksyon, ...update})
|
|
||||||
}, [tradiksyon])
|
|
||||||
|
|
||||||
const handleRemoveTradiksyon = useCallback(idTradiksyon => {
|
|
||||||
setKiChwalang(kiChawLang.filter(t => t !== idTradiksyon))
|
|
||||||
}, [kiChawLang, setKiChwalang])
|
|
||||||
|
|
||||||
const handleClose = (event, reason) => {
|
|
||||||
if (reason === 'clickaway') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setOpen(false)
|
|
||||||
setSuccess('')
|
|
||||||
setError('')
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleClick = async () => {
|
|
||||||
setLoading(true)
|
|
||||||
const {awtis, tit, transkripsyon} = teksEkri
|
|
||||||
const {fr, en, es, de, it} = tradiksyon
|
|
||||||
|
|
||||||
if (teksEkri.awtis === '' || teksEkri.tit === '' || teksEkri.transkripsyon === '') {
|
|
||||||
setError({message: 'Certains champs sont obligatoires'})
|
|
||||||
setLoading(false)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const headers = {
|
|
||||||
'content-type': 'application/json',
|
|
||||||
Authorization: `Bearer ${jwt}`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentTeksId) {
|
|
||||||
try {
|
|
||||||
const teks = await jwennAnTeks(currentTeksId, jwt)
|
|
||||||
const teksResponse = await axios.put(`${API_URL}/paroles/${currentTeksId}`, {
|
|
||||||
data: {
|
|
||||||
id: teks.id,
|
|
||||||
titre: tit,
|
|
||||||
transcription: transkripsyon,
|
|
||||||
traductions: {
|
|
||||||
francais: fr === '' ? null : fr,
|
|
||||||
anglais: en === '' ? null : en,
|
|
||||||
espagnol: es === '' ? null : es,
|
|
||||||
allemand: de === '' ? null : de,
|
|
||||||
italien: it === '' ? null : it
|
|
||||||
},
|
|
||||||
user: {
|
|
||||||
id: user.id,
|
|
||||||
username: user.username,
|
|
||||||
email: user.email
|
|
||||||
},
|
|
||||||
artistes: teks.artistes.map(({id}) => id),
|
|
||||||
traductionAuto: tradiksyonOtomatik
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
headers
|
|
||||||
})
|
|
||||||
|
|
||||||
const {data} = teksResponse
|
|
||||||
setSuccess(`Le morceau "${data.tit}" a été modifié avec succès. Il apparaîtra sur le site après validation.`)
|
|
||||||
setLoading(false)
|
|
||||||
} catch (error) {
|
|
||||||
setError(error?.response?.data)
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
const artiste = await axios.post(`${API_URL}/artistes`, {
|
|
||||||
data: {
|
|
||||||
alias: awtis,
|
|
||||||
user: {
|
|
||||||
id: user.id,
|
|
||||||
username: user.username,
|
|
||||||
email: user.email
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
headers
|
|
||||||
})
|
|
||||||
|
|
||||||
const parole = await axios.post(`${API_URL}/paroles`, {
|
|
||||||
data: {
|
|
||||||
titre: tit,
|
|
||||||
transcription: transkripsyon,
|
|
||||||
traductions: {
|
|
||||||
francais: fr === '' ? null : fr,
|
|
||||||
anglais: en === '' ? null : en,
|
|
||||||
espagnol: es === '' ? null : es,
|
|
||||||
allemand: de === '' ? null : de,
|
|
||||||
italien: it === '' ? null : it
|
|
||||||
},
|
|
||||||
user: {
|
|
||||||
id: user.id,
|
|
||||||
username: user.username,
|
|
||||||
email: user.email
|
|
||||||
},
|
|
||||||
artistes: [artiste.id],
|
|
||||||
traductionAuto: tradiksyonOtomatik
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
headers
|
|
||||||
})
|
|
||||||
|
|
||||||
const {data} = parole
|
|
||||||
|
|
||||||
setSuccess(`Le texte "${data.titre}" a été soumis avec succès. Il apparaîtra sur le site après validation.`)
|
|
||||||
setLoading(false)
|
|
||||||
} catch (error) {
|
|
||||||
setError(error?.response?.data)
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleReset = () => {
|
|
||||||
setTeksEkri({awtis: '', tit: '', transkripsyon: ''})
|
|
||||||
setTradiksyon({fr: '', en: '', es: '', de: '', it: ''})
|
|
||||||
setKiChwalang(['fr'])
|
|
||||||
setCurrentTeksId(null)
|
|
||||||
setTradiksyonOtomatik(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (success) {
|
|
||||||
setOpen(true)
|
|
||||||
handleReset()
|
|
||||||
}
|
|
||||||
}, [success])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (error) {
|
|
||||||
setOpen(true)
|
|
||||||
}
|
|
||||||
}, [error])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectedTeks) {
|
|
||||||
setTradiksyonOtomatik(false)
|
|
||||||
setCurrentTeksId(selectedTeks.id)
|
|
||||||
setTeksEkri({
|
|
||||||
awtis: new Intl.ListFormat('fr').format(selectedTeks.artistes.map(({alias}) => alias)),
|
|
||||||
tit: selectedTeks.titre,
|
|
||||||
transkripsyon: selectedTeks.transcription
|
|
||||||
})
|
|
||||||
setTradiksyon({
|
|
||||||
fr: selectedTeks?.traductions?.francais || '',
|
|
||||||
en: selectedTeks?.traductions?.anglais || '',
|
|
||||||
es: selectedTeks?.traductions?.espagnol || '',
|
|
||||||
de: selectedTeks?.traductions?.allemand || '',
|
|
||||||
it: selectedTeks?.traductions?.italien || ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const jennLangId = () => {
|
|
||||||
const ids = []
|
|
||||||
|
|
||||||
if (selectedTeks?.traductions?.francais) {
|
|
||||||
ids.push('fr')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedTeks?.traductions?.anglais) {
|
|
||||||
ids.push('en')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedTeks?.traductions?.espagnol) {
|
|
||||||
ids.push('es')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedTeks?.traductions?.allemand) {
|
|
||||||
ids.push('de')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedTeks?.traductions?.italien) {
|
|
||||||
ids.push('it')
|
|
||||||
}
|
|
||||||
|
|
||||||
return ids
|
|
||||||
}
|
|
||||||
|
|
||||||
const langIds = jennLangId()
|
|
||||||
|
|
||||||
setKiChwalang(langIds)
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
setSelectedTeks(null)
|
|
||||||
}
|
|
||||||
}, [teksEkri, selectedTeks, setSelectedTeks, kiChawLang])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StyledContainer maxWidth='sm'>
|
|
||||||
<Box sx={{textAlign: 'center', marginBottom: 2}}>
|
|
||||||
<Typography display='inline' variant='h6' component='h1'>
|
|
||||||
Proposer une parole
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<form noValidate autoComplete='off'>
|
|
||||||
<Grid container style={{textAlign: 'center'}} spacing={1}>
|
|
||||||
<Grid xs='true'>
|
|
||||||
<TextField
|
|
||||||
required
|
|
||||||
id='awtis'
|
|
||||||
name='awtis'
|
|
||||||
label='Artiste'
|
|
||||||
value={teksEkri.awtis}
|
|
||||||
disabled={Boolean(currentTeksId)}
|
|
||||||
variant='outlined'
|
|
||||||
helperText='Nom de l’artiste (obligatoire)'
|
|
||||||
onChange={event => handleUpdate({awtis: event.target.value})}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid xs='true'>
|
|
||||||
<TextField
|
|
||||||
required
|
|
||||||
id='tit'
|
|
||||||
name='tit'
|
|
||||||
label='Titre'
|
|
||||||
value={selectedTeks?.tit || teksEkri.tit}
|
|
||||||
variant='outlined'
|
|
||||||
helperText='Titre de la musique (obligatoire)'
|
|
||||||
onChange={event => handleUpdate({tit: event.target.value})}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
multiline
|
|
||||||
required
|
|
||||||
helperText='Transcription des paroles (obligatoire)'
|
|
||||||
style={{marginTop: '1em'}}
|
|
||||||
id='transkripsyon'
|
|
||||||
label='Transcription'
|
|
||||||
value={teksEkri.transkripsyon}
|
|
||||||
rows={8}
|
|
||||||
variant='outlined'
|
|
||||||
onChange={event => handleUpdate({transkripsyon: event.target.value})}
|
|
||||||
/>
|
|
||||||
{kiChawLang.length > 0 && kiChawLang.map(k => (
|
|
||||||
<FormControl key={k} fullWidth style={{marginBlock: 10}}>
|
|
||||||
<InputLabel htmlFor={`tradiksyon-${k}`}>
|
|
||||||
<span dangerouslySetInnerHTML={{__html: textLabels[k].emoji}} /> {textLabels[k].title}
|
|
||||||
</InputLabel>
|
|
||||||
<OutlinedInput
|
|
||||||
multiline
|
|
||||||
style={{marginTop: '1em'}}
|
|
||||||
id={`tradiksyon-${k}`}
|
|
||||||
name={`tradiksyon-${k}`}
|
|
||||||
label={`${textLabels[k].title}`}
|
|
||||||
value={tradiksyon[k]}
|
|
||||||
rows={8}
|
|
||||||
endAdornment={
|
|
||||||
<InputAdornment position='end'>
|
|
||||||
<RemoveTooltip
|
|
||||||
title='Cacher la fenêtre'
|
|
||||||
placement='left'
|
|
||||||
classes={{
|
|
||||||
tooltip: classes.tooltip
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
style={{bottom: 75}}
|
|
||||||
aria-label='remove-tradiksyon'
|
|
||||||
edge='end'
|
|
||||||
size='large'
|
|
||||||
onClick={() => handleRemoveTradiksyon(k)}
|
|
||||||
>
|
|
||||||
<VisibilityOffIcon />
|
|
||||||
</IconButton>
|
|
||||||
</RemoveTooltip>
|
|
||||||
</InputAdornment>
|
|
||||||
}
|
|
||||||
onChange={event => handleUpdateTradiksyon({[k]: event.target.value})}
|
|
||||||
/>
|
|
||||||
<FormHelperText id={`tradiksyon-${k}`}>{textLabels[k].help}</FormHelperText>
|
|
||||||
</FormControl>
|
|
||||||
))}
|
|
||||||
</form>
|
|
||||||
<AjouteTradiksyon
|
|
||||||
showSwitch={Boolean(canAutoTranslate) && Boolean(!currentTeksId)}
|
|
||||||
disableSwitch={tradiksyon.fr === '' || tradiksyon.en !== '' || tradiksyon.es !== '' || tradiksyon.de !== '' || tradiksyon.it !== ''}
|
|
||||||
tradiksyonOtomatik={tradiksyonOtomatik}
|
|
||||||
setTradiksyonOtomatik={setTradiksyonOtomatik}
|
|
||||||
chwaLang={kiChawLang}
|
|
||||||
setChwaLang={setKiChwalang}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
fullWidth
|
|
||||||
disabled={loading || teksEkri.awtis === '' || teksEkri.tit === '' || teksEkri.transkripsyon === ''}
|
|
||||||
style={{marginTop: 20}}
|
|
||||||
variant='contained'
|
|
||||||
color='primary'
|
|
||||||
onClick={handleClick}
|
|
||||||
>
|
|
||||||
<Typography style={{fontWeight: 'bold'}}>
|
|
||||||
Valider
|
|
||||||
</Typography>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
fullWidth
|
|
||||||
disabled={loading || (teksEkri.awtis === '' && teksEkri.tit === '' && teksEkri.transkripsyon === '')}
|
|
||||||
style={{marginTop: 20}}
|
|
||||||
variant='contained'
|
|
||||||
color='error'
|
|
||||||
onClick={handleReset}
|
|
||||||
>
|
|
||||||
<Typography style={{fontWeight: 'bold'}}>
|
|
||||||
Tout effacer
|
|
||||||
</Typography>
|
|
||||||
</Button>
|
|
||||||
{loading && <LinearProgress size={24} style={{width: '100%', marginBlock: '1em'}} />}
|
|
||||||
</div>
|
|
||||||
{success && (
|
|
||||||
<Snackbar open={open} autoHideDuration={10_000} onClose={handleClose}>
|
|
||||||
<Alert severity='success' onClose={handleClose}>
|
|
||||||
<strong>{success}</strong>
|
|
||||||
</Alert>
|
|
||||||
</Snackbar>
|
|
||||||
)}
|
|
||||||
{error && (
|
|
||||||
<Snackbar open={open} autoHideDuration={10_000} onClose={handleClose}>
|
|
||||||
<Alert severity='error' onClose={handleClose}>
|
|
||||||
<strong>Une erreur s’est produite</strong> : <i>{error?.error?.message}</i>
|
|
||||||
</Alert>
|
|
||||||
</Snackbar>
|
|
||||||
)}
|
|
||||||
</StyledContainer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
EkriTeks.propTypes = {
|
|
||||||
canAutoTranslate: PropTypes.bool,
|
|
||||||
selectedTeks: PropTypes.object,
|
|
||||||
setSelectedTeks: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default EkriTeks
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import PropTypes from 'prop-types'
|
|
||||||
import FormGroup from '@mui/material/FormGroup'
|
|
||||||
import FormControlLabel from '@mui/material/FormControlLabel'
|
|
||||||
import Switch from '@mui/material/Switch'
|
|
||||||
|
|
||||||
export default function OtomatikSwitch({tradiksyonOtomatik, setTradiksyonOtomatik, disabled}) {
|
|
||||||
const handleChange = event => {
|
|
||||||
setTradiksyonOtomatik(event.target.checked)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormGroup sx={{marginTop: 1}}>
|
|
||||||
<FormControlLabel
|
|
||||||
control={<Switch checked={tradiksyonOtomatik && !disabled} onChange={handleChange} />}
|
|
||||||
disabled={disabled}
|
|
||||||
label='Traduction du français vers les autres langues (remplir uniquement la traduction française)'
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
OtomatikSwitch.propTypes = {
|
|
||||||
tradiksyonOtomatik: PropTypes.bool.isRequired,
|
|
||||||
setTradiksyonOtomatik: PropTypes.func.isRequired,
|
|
||||||
disabled: PropTypes.bool.isRequired
|
|
||||||
}
|
|
||||||
@@ -4,8 +4,6 @@ import ArrowRightAltIcon from '@mui/icons-material/ArrowRightAlt'
|
|||||||
|
|
||||||
import CGUDialog from '../cgu/cgu-dialog'
|
import CGUDialog from '../cgu/cgu-dialog'
|
||||||
|
|
||||||
const CGU_PDF_LINK = process.env.NEXT_PUBLIC_CGU_DOWNLOAD_LINK
|
|
||||||
|
|
||||||
export default function Presantasyon() {
|
export default function Presantasyon() {
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
import PropTypes from 'prop-types'
|
|
||||||
import {styled} from '@mui/material/styles'
|
|
||||||
import Typography from '@mui/material/Typography'
|
|
||||||
import Box from '@mui/material/Box'
|
|
||||||
import LinearProgress, {linearProgressClasses} from '@mui/material/LinearProgress'
|
|
||||||
|
|
||||||
const BorderLinearProgress = styled(LinearProgress)(({theme}) => ({
|
|
||||||
height: 10,
|
|
||||||
borderRadius: 5,
|
|
||||||
[`&.${linearProgressClasses.colorPrimary}`]: {
|
|
||||||
backgroundColor: 200,
|
|
||||||
...theme.applyStyles('dark', {
|
|
||||||
backgroundColor: 800
|
|
||||||
})
|
|
||||||
},
|
|
||||||
[`& .${linearProgressClasses.bar}`]: {
|
|
||||||
borderRadius: 5,
|
|
||||||
backgroundColor: '#1a90ff',
|
|
||||||
...theme.applyStyles('dark', {
|
|
||||||
backgroundColor: '#308fe8'
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
export default function BarStats({percent}) {
|
|
||||||
return (
|
|
||||||
<Box sx={{display: 'flex', alignItems: 'center'}}>
|
|
||||||
<Box sx={{width: '100%'}}>
|
|
||||||
<BorderLinearProgress variant='determinate' value={percent} />
|
|
||||||
</Box>
|
|
||||||
<Box sx={{minWidth: 35}}>
|
|
||||||
<Typography textAlign='right' sx={{fontWeight: 'bold'}} variant='body2' color='text.secondary'>{`${Math.round(
|
|
||||||
percent,
|
|
||||||
)}%`}</Typography>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
BarStats.propTypes = {
|
|
||||||
percent: PropTypes.number.isRequired,
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import PropTypes from 'prop-types'
|
|
||||||
import Card from '@mui/material/Card'
|
|
||||||
import CardContent from '@mui/material/CardContent'
|
|
||||||
import Typography from '@mui/material/Typography'
|
|
||||||
import Grid from '@mui/material/Grid'
|
|
||||||
|
|
||||||
import TraductionsStats from './traductions-stats'
|
|
||||||
|
|
||||||
export default function KatStats({emoji, value, total}) {
|
|
||||||
return (
|
|
||||||
<Grid size={{xs: 12, md: 2}}>
|
|
||||||
<Card variant='outlined'>
|
|
||||||
<CardContent>
|
|
||||||
<Typography align='center' variant='h6'>
|
|
||||||
<span dangerouslySetInnerHTML={{__html: emoji}} />
|
|
||||||
</Typography>
|
|
||||||
<TraductionsStats value={value} total={total} />
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</Grid>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
KatStats.propTypes = {
|
|
||||||
emoji: PropTypes.string.isRequired,
|
|
||||||
value: PropTypes.number.isRequired,
|
|
||||||
total: PropTypes.number.isRequired
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import PropTypes from 'prop-types'
|
|
||||||
import Typography from '@mui/material/Typography'
|
|
||||||
import Box from '@mui/material/Box'
|
|
||||||
import Grid from '@mui/material/Grid'
|
|
||||||
|
|
||||||
import BarStats from './bar-stats'
|
|
||||||
|
|
||||||
export default function TraductionsStats({value, total}) {
|
|
||||||
const translated = total - value
|
|
||||||
const percent = (translated / total) * 100
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box sx={{width: '100%'}}>
|
|
||||||
<Grid container rowSpacing={3} columnSpacing={6} justifyContent='space-around'>
|
|
||||||
<Grid size={{xs: 12, md: 6}}>
|
|
||||||
<Typography style={{fontWeight: 'bold'}} align='center' variant='h6'>
|
|
||||||
{translated}
|
|
||||||
</Typography>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
<BarStats value={value} percent={percent} />
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
TraductionsStats.propTypes = {
|
|
||||||
value: PropTypes.number.isRequired,
|
|
||||||
total: PropTypes.number.isRequired
|
|
||||||
}
|
|
||||||
@@ -1,18 +1,19 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import OpenInNewIcon from '@mui/icons-material/OpenInNew'
|
import OpenInNewIcon from '@mui/icons-material/OpenInNew'
|
||||||
import {Spotify, Applemusic, Deezer, Tidal, Youtubemusic, Amazon, Soundcloud} from '@icons-pack/react-simple-icons'
|
import {Spotify, Applemusic, Deezer, Tidal, Youtubemusic, Amazon, Soundcloud} from '@icons-pack/react-simple-icons'
|
||||||
|
|
||||||
export const PLATFORM_CONFIG = {
|
export const PLATFORM_CONFIG = {
|
||||||
Spotify: {label: 'Spotify', bg: '#1DB954', color: '#fff', Icon: Spotify},
|
Spotify: {label: 'Spotify', bg: '#1DB954', color: '#fff', Icon: Spotify},
|
||||||
Applemusic: {label: 'Apple Music', bg: '#FC3C44', color: '#fff', Icon: Applemusic},
|
Applemusic: {label: 'Apple Music', bg: '#FC3C44', color: '#fff', Icon: Applemusic},
|
||||||
Deezer: {label: 'Deezer', bg: '#EF5466', color: '#fff', Icon: Deezer},
|
Deezer: {label: 'Deezer', bg: '#EF5466', color: '#fff', Icon: Deezer},
|
||||||
Tidal: {label: 'Tidal', bg: '#000000', color: '#fff', Icon: Tidal},
|
Tidal: {label: 'Tidal', bg: '#000000', color: '#fff', Icon: Tidal},
|
||||||
Qobuz: {label: 'Qobuz', bg: '#00245B', color: '#fff', Icon: null},
|
Qobuz: {label: 'Qobuz', bg: '#00245B', color: '#fff', Icon: null},
|
||||||
Youtubemusic: {label: 'YouTube Music', bg: '#FF0000', color: '#fff', Icon: Youtubemusic},
|
Youtubemusic: {label: 'YouTube Music', bg: '#FF0000', color: '#fff', Icon: Youtubemusic},
|
||||||
Amazon: {label: 'Amazon Music', bg: '#00A8E1', color: '#fff', Icon: Amazon},
|
Amazon: {label: 'Amazon Music', bg: '#00A8E1', color: '#fff', Icon: Amazon},
|
||||||
Soundcloud: {label: 'SoundCloud', bg: '#FF5500', color: '#fff', Icon: Soundcloud},
|
Soundcloud: {label: 'SoundCloud', bg: '#FF5500', color: '#fff', Icon: Soundcloud},
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StreamButton({lyen}) {
|
export function StreamButton({lyen}) {
|
||||||
@@ -39,3 +40,10 @@ export function StreamButton({lyen}) {
|
|||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StreamButton.propTypes = {
|
||||||
|
lyen: PropTypes.shape({
|
||||||
|
plateforme: PropTypes.string.isRequired,
|
||||||
|
url: PropTypes.string.isRequired
|
||||||
|
}).isRequired
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import {createContext, useContext, useState} from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
const CurrentTrackContext = createContext([null, () => {}])
|
||||||
|
|
||||||
|
export function CurrentTrackProvider({children}) {
|
||||||
|
const state = useState(null)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CurrentTrackContext.Provider value={state}>
|
||||||
|
{children}
|
||||||
|
</CurrentTrackContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCurrentTrack() {
|
||||||
|
return useContext(CurrentTrackContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrentTrackProvider.propTypes = {
|
||||||
|
children: PropTypes.node.isRequired
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import {Box, IconButton} from '@mui/material'
|
import {Box, IconButton} from '@mui/material'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import {useRouter} from 'next/navigation'
|
import {usePathname} from 'next/navigation'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Tidal,
|
Tidal,
|
||||||
@@ -69,10 +69,12 @@ function RannIframe({plateforme, url, titre, isMobile}) {
|
|||||||
{plateforme === 'Tidal' && (
|
{plateforme === 'Tidal' && (
|
||||||
<iframe
|
<iframe
|
||||||
src={src}
|
src={src}
|
||||||
|
title={titre}
|
||||||
allowFullScreen='allowfullscreen'
|
allowFullScreen='allowfullscreen'
|
||||||
frameBorder='0'
|
frameBorder='0'
|
||||||
width={`${isMobile ? '100%' : '50%'}`}
|
width={`${isMobile ? '100%' : '50%'}`}
|
||||||
height='96px'
|
height='96px'
|
||||||
|
sandbox='allow-same-origin allow-scripts allow-popups'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -90,38 +92,44 @@ function RannIframe({plateforme, url, titre, isMobile}) {
|
|||||||
|
|
||||||
{plateforme === 'Deezer' && (
|
{plateforme === 'Deezer' && (
|
||||||
<iframe
|
<iframe
|
||||||
title='deezer-widget'
|
title={titre || 'deezer-widget'}
|
||||||
frameBorder='0'
|
frameBorder='0'
|
||||||
src={src}
|
src={src}
|
||||||
allow='encrypted-media; clipboard-write'
|
allow='encrypted-media; clipboard-write'
|
||||||
allowtransparency='true'
|
allowtransparency='true'
|
||||||
|
sandbox='allow-same-origin allow-scripts allow-popups'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{plateforme === 'Spotify' && (
|
{plateforme === 'Spotify' && (
|
||||||
<iframe
|
<iframe
|
||||||
src={src}
|
src={src}
|
||||||
|
title={titre}
|
||||||
width={`${isMobile ? '100%' : '50%'}`}
|
width={`${isMobile ? '100%' : '50%'}`}
|
||||||
height='80'
|
height='80'
|
||||||
frameBorder='0'
|
frameBorder='0'
|
||||||
allowtransparency='true'
|
allowtransparency='true'
|
||||||
allow='encrypted-media'
|
allow='encrypted-media'
|
||||||
|
sandbox='allow-same-origin allow-scripts allow-popups'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{plateforme === 'Soundcloud' && (
|
{plateforme === 'Soundcloud' && (
|
||||||
<iframe
|
<iframe
|
||||||
src={src}
|
src={src}
|
||||||
|
title={titre}
|
||||||
width={`${isMobile ? '100%' : '50%'}`}
|
width={`${isMobile ? '100%' : '50%'}`}
|
||||||
height='166'
|
height='166'
|
||||||
scrolling='no'
|
scrolling='no'
|
||||||
frameBorder='no'
|
frameBorder='no'
|
||||||
|
sandbox='allow-same-origin allow-scripts allow-popups'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{plateforme === 'Applemusic' && (
|
{plateforme === 'Applemusic' && (
|
||||||
<iframe
|
<iframe
|
||||||
src={src}
|
src={src}
|
||||||
|
title={titre}
|
||||||
width={`${isMobile ? '100%' : '50%'}`}
|
width={`${isMobile ? '100%' : '50%'}`}
|
||||||
height='175'
|
height='175'
|
||||||
frameBorder='0'
|
frameBorder='0'
|
||||||
@@ -136,7 +144,7 @@ function RannIframe({plateforme, url, titre, isMobile}) {
|
|||||||
|
|
||||||
function EntegreMizik({parole, isMobile}) {
|
function EntegreMizik({parole, isMobile}) {
|
||||||
const [chwaMizik, meteChwaMizik] = useState(null)
|
const [chwaMizik, meteChwaMizik] = useState(null)
|
||||||
const router = useRouter()
|
const pathname = usePathname()
|
||||||
const {streamAudio, slug, okiMizikID, gadeEmbed, titre} = parole
|
const {streamAudio, slug, okiMizikID, gadeEmbed, titre} = parole
|
||||||
|
|
||||||
const filteredKouteyAchtey = streamAudio.filter(({plateforme}) => plateforme === 'Tidal' || plateforme === 'Spotify' || plateforme === 'Deezer' || plateforme === 'Soundcloud' || plateforme === 'Applemusic')
|
const filteredKouteyAchtey = streamAudio.filter(({plateforme}) => plateforme === 'Tidal' || plateforme === 'Spotify' || plateforme === 'Deezer' || plateforme === 'Soundcloud' || plateforme === 'Applemusic')
|
||||||
@@ -150,10 +158,10 @@ function EntegreMizik({parole, isMobile}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (router.asPath === `/paroles/${slug}` || router.asPath === `/paroles/${slug}#${slug}`) {
|
if (pathname === `/paroles/${slug}`) {
|
||||||
meteChwaMizik(null)
|
meteChwaMizik(null)
|
||||||
}
|
}
|
||||||
}, [router, slug])
|
}, [pathname, slug])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box align='center'>
|
<Box align='center'>
|
||||||
@@ -162,7 +170,7 @@ function EntegreMizik({parole, isMobile}) {
|
|||||||
key={id}
|
key={id}
|
||||||
aria-label='player'
|
aria-label='player'
|
||||||
size='large'
|
size='large'
|
||||||
onClick={() => handleClick(plateforme, url)}
|
onClick={() => handleClick(plateforme, url, titre)}
|
||||||
>
|
>
|
||||||
{kouteyAchteyIcons[plateforme]}
|
{kouteyAchteyIcons[plateforme]}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const pulse = keyframes`
|
|||||||
`
|
`
|
||||||
|
|
||||||
const SlideUp = forwardRef(function SlideUp(props, ref) {
|
const SlideUp = forwardRef(function SlideUp(props, ref) {
|
||||||
return <Slide direction='up' ref={ref} {...props} />
|
return <Slide ref={ref} direction='up' {...props} />
|
||||||
})
|
})
|
||||||
|
|
||||||
function toEmbedUrl(url) {
|
function toEmbedUrl(url) {
|
||||||
@@ -31,6 +31,7 @@ function toEmbedUrl(url) {
|
|||||||
if (match) {
|
if (match) {
|
||||||
return `${u.origin}/videos/embed/${match[1]}?title=0&warningTitle=0&peertubeLink=0&controlBar=1`
|
return `${u.origin}/videos/embed/${match[1]}?title=0&warningTitle=0&peertubeLink=0&controlBar=1`
|
||||||
}
|
}
|
||||||
|
|
||||||
return url
|
return url
|
||||||
} catch {
|
} catch {
|
||||||
return url
|
return url
|
||||||
@@ -45,7 +46,9 @@ export default function KaraokeModal({url, desktopUrl, titre, artistes}) {
|
|||||||
const activeUrl = (!isMobile && desktopUrl) ? desktopUrl : url
|
const activeUrl = (!isMobile && desktopUrl) ? desktopUrl : url
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) setLoaded(false)
|
if (!open) {
|
||||||
|
setLoaded(false)
|
||||||
|
}
|
||||||
}, [open])
|
}, [open])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -55,13 +58,13 @@ export default function KaraokeModal({url, desktopUrl, titre, artistes}) {
|
|||||||
color='primary'
|
color='primary'
|
||||||
size={isMobile ? 'medium' : 'large'}
|
size={isMobile ? 'medium' : 'large'}
|
||||||
aria-label='karaoké'
|
aria-label='karaoké'
|
||||||
onClick={() => setOpen(true)}
|
|
||||||
sx={{
|
sx={{
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
bottom: {xs: 24, sm: 32},
|
bottom: {xs: 24, sm: 32},
|
||||||
right: {xs: 24, sm: 32},
|
right: {xs: 24, sm: 32},
|
||||||
zIndex: 1050,
|
zIndex: 1050,
|
||||||
}}
|
}}
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
>
|
>
|
||||||
<MicIcon />
|
<MicIcon />
|
||||||
</Fab>
|
</Fab>
|
||||||
@@ -69,7 +72,6 @@ export default function KaraokeModal({url, desktopUrl, titre, artistes}) {
|
|||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
open={open}
|
open={open}
|
||||||
onClose={() => setOpen(false)}
|
|
||||||
slots={{transition: SlideUp}}
|
slots={{transition: SlideUp}}
|
||||||
maxWidth={false}
|
maxWidth={false}
|
||||||
sx={{
|
sx={{
|
||||||
@@ -102,6 +104,7 @@ export default function KaraokeModal({url, desktopUrl, titre, artistes}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onClose={() => setOpen(false)}
|
||||||
>
|
>
|
||||||
{/* Header superposé en gradient */}
|
{/* Header superposé en gradient */}
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
@@ -118,7 +121,8 @@ export default function KaraokeModal({url, desktopUrl, titre, artistes}) {
|
|||||||
alignItems: 'flex-start',
|
alignItems: 'flex-start',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<Box sx={{pointerEvents: 'none'}}>
|
<Box sx={{pointerEvents: 'none'}}>
|
||||||
{titre && (
|
{titre && (
|
||||||
<Typography variant='subtitle1' sx={{color: '#fff', fontWeight: 700, lineHeight: 1.2, textShadow: '0 1px 4px rgba(0,0,0,0.5)'}}>
|
<Typography variant='subtitle1' sx={{color: '#fff', fontWeight: 700, lineHeight: 1.2, textShadow: '0 1px 4px rgba(0,0,0,0.5)'}}>
|
||||||
@@ -133,7 +137,6 @@ export default function KaraokeModal({url, desktopUrl, titre, artistes}) {
|
|||||||
</Box>
|
</Box>
|
||||||
<IconButton
|
<IconButton
|
||||||
size='small'
|
size='small'
|
||||||
onClick={() => setOpen(false)}
|
|
||||||
aria-label='fermer'
|
aria-label='fermer'
|
||||||
sx={{
|
sx={{
|
||||||
pointerEvents: 'auto',
|
pointerEvents: 'auto',
|
||||||
@@ -143,6 +146,7 @@ export default function KaraokeModal({url, desktopUrl, titre, artistes}) {
|
|||||||
border: '1px solid rgba(255,255,255,0.12)',
|
border: '1px solid rgba(255,255,255,0.12)',
|
||||||
'&:hover': {bgcolor: 'rgba(255,255,255,0.25)'},
|
'&:hover': {bgcolor: 'rgba(255,255,255,0.25)'},
|
||||||
}}
|
}}
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
>
|
>
|
||||||
<CloseIcon fontSize='small' />
|
<CloseIcon fontSize='small' />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -166,7 +170,8 @@ export default function KaraokeModal({url, desktopUrl, titre, artistes}) {
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
gap: 1.5,
|
gap: 1.5,
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<MicIcon sx={{fontSize: 44, color: 'rgba(255,255,255,0.55)', animation: `${pulse} 1.6s ease-in-out infinite`}} />
|
<MicIcon sx={{fontSize: 44, color: 'rgba(255,255,255,0.55)', animation: `${pulse} 1.6s ease-in-out infinite`}} />
|
||||||
<Typography
|
<Typography
|
||||||
variant='caption'
|
variant='caption'
|
||||||
@@ -187,27 +192,28 @@ export default function KaraokeModal({url, desktopUrl, titre, artistes}) {
|
|||||||
transform: 'translateX(-50%)',
|
transform: 'translateX(-50%)',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
aspectRatio: '16 / 9',
|
aspectRatio: '16 / 9',
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<iframe
|
<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
|
allowFullScreen
|
||||||
|
src={toEmbedUrl(activeUrl)}
|
||||||
|
style={{width: '100%', height: '100%', border: 'none', display: 'block', opacity: loaded ? 1 : 0, transition: 'opacity 0.4s ease'}}
|
||||||
allow='autoplay; fullscreen'
|
allow='autoplay; fullscreen'
|
||||||
sandbox='allow-same-origin allow-scripts allow-popups allow-forms'
|
sandbox='allow-same-origin allow-scripts allow-popups allow-forms'
|
||||||
title='Karaoké'
|
title='Karaoké'
|
||||||
|
onLoad={() => setLoaded(true)}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
/* Vidéo 16:9 desktop : player remplit exactement le dialog paysage */
|
/* Vidéo 16:9 desktop : player remplit exactement le dialog paysage */
|
||||||
<iframe
|
<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
|
allowFullScreen
|
||||||
|
src={toEmbedUrl(activeUrl)}
|
||||||
|
style={{width: '100%', height: '100%', border: 'none', display: 'block', opacity: loaded ? 1 : 0, transition: 'opacity 0.4s ease'}}
|
||||||
allow='autoplay; fullscreen'
|
allow='autoplay; fullscreen'
|
||||||
sandbox='allow-same-origin allow-scripts allow-popups allow-forms'
|
sandbox='allow-same-origin allow-scripts allow-popups allow-forms'
|
||||||
title='Karaoké'
|
title='Karaoké'
|
||||||
|
onLoad={() => setLoaded(true)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
+33
-30
@@ -16,16 +16,24 @@ import Image from 'next/image'
|
|||||||
import {grey} from '@mui/material/colors'
|
import {grey} from '@mui/material/colors'
|
||||||
import {Link} from '@mui/material'
|
import {Link} from '@mui/material'
|
||||||
|
|
||||||
import {getAlias} from '../../lib/utils/format'
|
import {getAlias} from '../../lib/utils/get-alias'
|
||||||
|
import {formatKuveti} from '../../lib/kuveti'
|
||||||
|
|
||||||
const IMAGE_URL = process.env.NEXT_PUBLIC_API_URL_ROOT || 'http://localhost:1337'
|
const IMAGE_URL = process.env.NEXT_PUBLIC_API_URL_ROOT || 'http://localhost:1337'
|
||||||
|
|
||||||
|
function createAudio(src) {
|
||||||
|
const audio = new Audio(src)
|
||||||
|
audio.preload = 'metadata'
|
||||||
|
return audio
|
||||||
|
}
|
||||||
|
|
||||||
function parseLrc(text) {
|
function parseLrc(text) {
|
||||||
const lines = []
|
const lines = []
|
||||||
for (const raw of text.split('\n')) {
|
for (const raw of text.split('\n')) {
|
||||||
const match = raw.match(/^\[(\d{2}):(\d{2})[.:]\d+\](.*)$/)
|
const match = raw.match(/^\[(\d{2}):(\d{2})[.:]\d+](.*)$/)
|
||||||
if (match) {
|
if (match) {
|
||||||
lines.push({time: parseInt(match[1]) * 60 + parseInt(match[2]), text: match[3].trim()})
|
const time = (Number.parseInt(match[1], 10) * 60) + Number.parseInt(match[2], 10)
|
||||||
|
lines.push({time, text: match[3].trim()})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,9 +76,12 @@ const TinyText = styled(Typography)({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export default function Lekte({audio, url, parole}) {
|
export default function Lekte({audio, url, parole}) {
|
||||||
const audioRef = useRef(new Audio(audio))
|
const audioRef = useRef(null)
|
||||||
|
if (audioRef.current === null) {
|
||||||
|
audioRef.current = createAudio(audio)
|
||||||
|
}
|
||||||
|
|
||||||
const intervalRef = useRef()
|
const intervalRef = useRef()
|
||||||
const isReady = useRef(false)
|
|
||||||
const [duration, setDuration] = useState(0)
|
const [duration, setDuration] = useState(0)
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const {mode} = useColorScheme()
|
const {mode} = useColorScheme()
|
||||||
@@ -97,25 +108,9 @@ export default function Lekte({audio, url, parole}) {
|
|||||||
clearInterval(intervalRef.current)
|
clearInterval(intervalRef.current)
|
||||||
intervalRef.current = setInterval(() => {
|
intervalRef.current = setInterval(() => {
|
||||||
setPosition(Math.round(audioRef.current.currentTime))
|
setPosition(Math.round(audioRef.current.currentTime))
|
||||||
}, [1000])
|
}, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isReady.current) {
|
|
||||||
audioRef.current.play()
|
|
||||||
setIsPlaying(true)
|
|
||||||
startTimer()
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isPlaying) {
|
|
||||||
audioRef.current.play()
|
|
||||||
} else {
|
|
||||||
audioRef.current.pause()
|
|
||||||
}
|
|
||||||
}, [isPlaying])
|
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
() => {
|
() => {
|
||||||
audioRef.current.pause()
|
audioRef.current.pause()
|
||||||
@@ -125,7 +120,7 @@ export default function Lekte({audio, url, parole}) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
audioRef.current.play()
|
audioRef.current.play().catch(() => {})
|
||||||
startTimer()
|
startTimer()
|
||||||
} else {
|
} else {
|
||||||
clearInterval(intervalRef.current)
|
clearInterval(intervalRef.current)
|
||||||
@@ -135,7 +130,7 @@ export default function Lekte({audio, url, parole}) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
audioRef.current.pause()
|
audioRef.current.pause()
|
||||||
audioRef.current = new Audio(audio)
|
audioRef.current = createAudio(audio)
|
||||||
setIsPlaying(false)
|
setIsPlaying(false)
|
||||||
setVolume(100)
|
setVolume(100)
|
||||||
setPosition(0)
|
setPosition(0)
|
||||||
@@ -151,6 +146,7 @@ export default function Lekte({audio, url, parole}) {
|
|||||||
setPosition(0)
|
setPosition(0)
|
||||||
el.currentTime = 0
|
el.currentTime = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
el.addEventListener('loadedmetadata', onLoaded)
|
el.addEventListener('loadedmetadata', onLoaded)
|
||||||
el.addEventListener('ended', onEnded)
|
el.addEventListener('ended', onEnded)
|
||||||
return () => {
|
return () => {
|
||||||
@@ -160,7 +156,10 @@ export default function Lekte({audio, url, parole}) {
|
|||||||
}, [audio])
|
}, [audio])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!parole?.pawol?.url) return
|
if (!parole?.pawol?.url) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
fetch(new URL(parole.pawol.url, IMAGE_URL).toString())
|
fetch(new URL(parole.pawol.url, IMAGE_URL).toString())
|
||||||
.then(r => r.text())
|
.then(r => r.text())
|
||||||
.then(text => setLrcLines(parseLrc(text)))
|
.then(text => setLrcLines(parseLrc(text)))
|
||||||
@@ -168,7 +167,7 @@ export default function Lekte({audio, url, parole}) {
|
|||||||
}, [parole?.pawol?.url])
|
}, [parole?.pawol?.url])
|
||||||
|
|
||||||
const activeIndex = lrcLines
|
const activeIndex = lrcLines
|
||||||
? lrcLines.reduce((last, line, i) => line.time <= position ? i : last, -1)
|
? lrcLines.findLastIndex(line => line.time <= position)
|
||||||
: -1
|
: -1
|
||||||
|
|
||||||
const handleChangePosition = value => {
|
const handleChangePosition = value => {
|
||||||
@@ -181,9 +180,10 @@ export default function Lekte({audio, url, parole}) {
|
|||||||
audioRef.current.volume = value / 100
|
audioRef.current.volume = value / 100
|
||||||
}
|
}
|
||||||
|
|
||||||
const imagePath = parole?.couverture?.formats?.thumbnail?.url
|
const couvertureFormat = formatKuveti(parole?.couverture, 'thumbnail')
|
||||||
const width = parole?.couverture?.formats?.thumbnail?.width || 192
|
const imagePath = couvertureFormat?.url
|
||||||
const height = parole?.couverture?.formats?.thumbnail?.height || 192
|
const width = couvertureFormat?.width || 192
|
||||||
|
const height = couvertureFormat?.height || 192
|
||||||
|
|
||||||
const imageSrc = imagePath
|
const imageSrc = imagePath
|
||||||
? new URL(imagePath, IMAGE_URL).toString()
|
? new URL(imagePath, IMAGE_URL).toString()
|
||||||
@@ -322,7 +322,10 @@ export default function Lekte({audio, url, parole}) {
|
|||||||
{[-1, 0, 1].map(offset => {
|
{[-1, 0, 1].map(offset => {
|
||||||
const idx = activeIndex + offset
|
const idx = activeIndex + offset
|
||||||
const line = lrcLines[idx]
|
const line = lrcLines[idx]
|
||||||
if (!line) return <Box key={offset} sx={{height: offset === 0 ? 28 : 20}} />
|
if (!line) {
|
||||||
|
return <Box key={offset} sx={{height: offset === 0 ? 28 : 20}} />
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Typography
|
<Typography
|
||||||
key={offset}
|
key={offset}
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import {Box, CircularProgress} from '@mui/material'
|
|||||||
import dynamic from 'next/dynamic'
|
import dynamic from 'next/dynamic'
|
||||||
|
|
||||||
const MIZIK_URL = process.env.NEXT_PUBLIC_OKI_MIZIK_URL || 'https://funkwhale-server.com'
|
const MIZIK_URL = process.env.NEXT_PUBLIC_OKI_MIZIK_URL || 'https://funkwhale-server.com'
|
||||||
const MIZIK_API_USER = process.env.NEXT_PUBLIC_MIZIK_API_USER || 'user'
|
|
||||||
const MIZIK_API_PASSWORD = process.env.NEXT_PUBLIC_MIZIK_API_PASSWORD || 'password'
|
|
||||||
|
|
||||||
const DinamikLekte = dynamic(
|
const DinamikLekte = dynamic(
|
||||||
() => import('./lekte'), // eslint-disable-line node/no-unsupported-features/es-syntax
|
() => import('./lekte'), // eslint-disable-line node/no-unsupported-features/es-syntax
|
||||||
@@ -12,7 +10,7 @@ const DinamikLekte = dynamic(
|
|||||||
)
|
)
|
||||||
|
|
||||||
export default function OkiMizik({id, parole}) {
|
export default function OkiMizik({id, parole}) {
|
||||||
const mizikStreamUrl = `${MIZIK_URL}/rest/stream?u=${MIZIK_API_USER}&p=${MIZIK_API_PASSWORD}&id=${id}`
|
const mizikStreamUrl = `/api/mizik-stream/${id}`
|
||||||
const detailsUrl = `${MIZIK_URL}/library/tracks/${id}`
|
const detailsUrl = `${MIZIK_URL}/library/tracks/${id}`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import {notFound} from 'next/navigation'
|
||||||
|
import {jwennTeks} from '../../lib/oki-api'
|
||||||
|
import TeksDrawer from './teks-drawer'
|
||||||
|
|
||||||
|
export default async function TeksDrawerLoader() {
|
||||||
|
const teks = await jwennTeks()
|
||||||
|
|
||||||
|
if (!teks) {
|
||||||
|
notFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
return <TeksDrawer paroles={teks} />
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import {useState, useEffect, forwardRef} from 'react'
|
import {useState, useEffect, forwardRef} from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import {useParams} from 'next/navigation'
|
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import AppBar from '@mui/material/AppBar'
|
import AppBar from '@mui/material/AppBar'
|
||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
@@ -19,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'
|
import KaraokeModal from './karaoke-modal'
|
||||||
|
import {useCurrentTrack} from './current-track-context'
|
||||||
|
|
||||||
const drawerWidth = 240
|
const drawerWidth = 240
|
||||||
|
|
||||||
@@ -27,14 +27,12 @@ const Alert = forwardRef(function Alert(props, ref) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
export default function TeksDrawer({paroles}) {
|
export default function TeksDrawer({paroles}) {
|
||||||
const {slug} = useParams()
|
const [parole] = useCurrentTrack()
|
||||||
const [mobileOpen, setMobileOpen] = useState(false)
|
const [mobileOpen, setMobileOpen] = useState(false)
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [success, setSuccess] = useState('')
|
const [success, setSuccess] = useState('')
|
||||||
|
|
||||||
const parole = paroles.find((parole) => parole.slug === slug)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (error || success) {
|
if (error || success) {
|
||||||
setOpen(true)
|
setOpen(true)
|
||||||
@@ -84,7 +82,7 @@ export default function TeksDrawer({paroles}) {
|
|||||||
<MenuIcon />
|
<MenuIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
{parole && (
|
{parole && (
|
||||||
<Link passHref href='/paroles'>
|
<Link passHref prefetch={false} href='/paroles'>
|
||||||
<IconButton aria-label='return' size='medium'>
|
<IconButton aria-label='return' size='medium'>
|
||||||
<KeyboardBackspaceIcon style={{fontSize: '1.5em'}} />
|
<KeyboardBackspaceIcon style={{fontSize: '1.5em'}} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import Grid from '@mui/material/Grid'
|
|||||||
import ExplicitIcon from '@mui/icons-material/Explicit'
|
import ExplicitIcon from '@mui/icons-material/Explicit'
|
||||||
import {styled} from '@mui/material/styles'
|
import {styled} from '@mui/material/styles'
|
||||||
|
|
||||||
import {getAlias} from '../../lib/utils/format'
|
import {getAlias} from '../../lib/utils/get-alias'
|
||||||
|
import {formatKuveti} from '../../lib/kuveti'
|
||||||
|
|
||||||
const PREFIX = 'teks-kat'
|
const PREFIX = 'teks-kat'
|
||||||
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'
|
||||||
@@ -50,9 +51,9 @@ export default function TeksKat({parole}) {
|
|||||||
<Box sx={{position: 'relative', height: 240}}>
|
<Box sx={{position: 'relative', height: 240}}>
|
||||||
{couverture?.url ? (
|
{couverture?.url ? (
|
||||||
<Image
|
<Image
|
||||||
src={`${IMAGE_URL}${couverture.formats?.thumbnail?.url || couverture.url}`}
|
|
||||||
alt={titre}
|
|
||||||
fill
|
fill
|
||||||
|
src={`${IMAGE_URL}${formatKuveti(couverture, 'thumbnail')?.url}`}
|
||||||
|
alt={titre}
|
||||||
placeholder='blur'
|
placeholder='blur'
|
||||||
blurDataURL={BLUR_DATA_URL}
|
blurDataURL={BLUR_DATA_URL}
|
||||||
sizes='(max-width: 600px) 100vw, (max-width: 900px) 50vw, 33vw'
|
sizes='(max-width: 600px) 100vw, (max-width: 900px) 50vw, 33vw'
|
||||||
|
|||||||
+99
-78
@@ -1,9 +1,10 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import {useEffect} from 'react'
|
import {useEffect, useState} from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Grid from '@mui/material/Grid'
|
import Tabs from '@mui/material/Tabs'
|
||||||
|
import Tab from '@mui/material/Tab'
|
||||||
import Typography from '@mui/material/Typography'
|
import Typography from '@mui/material/Typography'
|
||||||
import Tooltip from '@mui/material/Tooltip'
|
import Tooltip from '@mui/material/Tooltip'
|
||||||
import {useMediaQuery} from '@mui/material'
|
import {useMediaQuery} from '@mui/material'
|
||||||
@@ -12,18 +13,23 @@ import slugify from 'slugify'
|
|||||||
|
|
||||||
import {styled} from '@mui/material/styles'
|
import {styled} from '@mui/material/styles'
|
||||||
import ExplicitIcon from '@mui/icons-material/Explicit'
|
import ExplicitIcon from '@mui/icons-material/Explicit'
|
||||||
|
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew'
|
||||||
|
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'
|
||||||
|
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
|
|
||||||
import {formatJsonString, getAlias} from '../../lib/utils/format'
|
import {formatJsonString} from '../../lib/utils/format'
|
||||||
|
import {getAlias} from '../../lib/utils/get-alias'
|
||||||
import {formatKuveti} from '../../lib/kuveti'
|
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 KitsDialog from '../files/kits-dialog'
|
||||||
import {StreamButton} from '../streaming-buttons'
|
import {StreamButton} from '../streaming-buttons'
|
||||||
import EntegreMizik from './entegre-mizik'
|
import EntegreMizik from './entegre-mizik'
|
||||||
import OkiMizik from './oki-mizik'
|
import OkiMizik from './oki-mizik'
|
||||||
import DiferansDialog from './diferans-dialog'
|
import DiferansDialog from './diferans-dialog'
|
||||||
|
import {useCurrentTrack} from './current-track-context'
|
||||||
|
|
||||||
const IMAGE_URL = process.env.NEXT_PUBLIC_API_URL_ROOT || 'http://localhost:1337'
|
const IMAGE_URL = process.env.NEXT_PUBLIC_API_URL_ROOT || 'http://localhost:1337'
|
||||||
|
|
||||||
@@ -78,54 +84,30 @@ const Root = styled('div')((
|
|||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const LANG_NAMES = {fr: 'Français', en: 'English', es: 'Español', de: 'Deutsch', it: 'Italiano'}
|
const LANG_NAMES = {
|
||||||
|
fr: 'Français', en: 'English', es: 'Español', de: 'Deutsch', it: 'Italiano',
|
||||||
const langToArray = parole => {
|
pt: 'Português', ja: '日本語', ko: '한국어',
|
||||||
const langArray = []
|
|
||||||
|
|
||||||
if (parole && parole.traductions) {
|
|
||||||
const {francais, anglais, espagnol, allemand, italien} = parole.traductions
|
|
||||||
|
|
||||||
if (anglais) {
|
|
||||||
langArray.push({title: 'English', lang: anglais})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (francais) {
|
|
||||||
langArray.push({title: 'Français', lang: francais})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (espagnol) {
|
|
||||||
langArray.push({title: 'Español', lang: espagnol})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allemand) {
|
|
||||||
langArray.push({title: 'Deutsch', lang: allemand})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (italien) {
|
|
||||||
langArray.push({title: 'Italiano', lang: italien})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return langArray
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const alignTeks = (langArray, isMobile) => {
|
const TRAD_FIELDS = [
|
||||||
if (langArray.length > 0 && !isMobile) {
|
{field: 'anglais', title: 'English'},
|
||||||
return 'justify'
|
{field: 'francais', title: 'Français'},
|
||||||
|
{field: 'espagnol', title: 'Español'},
|
||||||
|
{field: 'allemand', title: 'Deutsch'},
|
||||||
|
{field: 'italien', title: 'Italiano'},
|
||||||
|
{field: 'portugais', title: 'Português'},
|
||||||
|
{field: 'japonais', title: '日本語'},
|
||||||
|
{field: 'coreen', title: '한국어'},
|
||||||
|
]
|
||||||
|
|
||||||
|
const langToArray = parole => {
|
||||||
|
if (!parole || !parole.traductions) {
|
||||||
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
if (langArray.length > 0 && isMobile) {
|
return TRAD_FIELDS
|
||||||
return 'justify'
|
.filter(({field}) => parole.traductions[field])
|
||||||
}
|
.map(({field, title}) => ({title, lang: parole.traductions[field]}))
|
||||||
|
|
||||||
if (langArray.length === 0 && isMobile) {
|
|
||||||
return 'justify'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (langArray.length === 0 && !isMobile) {
|
|
||||||
return 'center'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ExplicitTooltip = Tooltip
|
const ExplicitTooltip = Tooltip
|
||||||
@@ -135,6 +117,8 @@ export default function Teks({parole}) {
|
|||||||
const langArray = langToArray(parole)
|
const langArray = langToArray(parole)
|
||||||
const enhancedAliases = getAlias(parole.artistes, parole.prioriteArtistes, true)
|
const enhancedAliases = getAlias(parole.artistes, parole.prioriteArtistes, true)
|
||||||
const coverFmt = formatKuveti(parole.couverture)
|
const coverFmt = formatKuveti(parole.couverture)
|
||||||
|
const [tab, setTab] = useState(0)
|
||||||
|
const [, setCurrentTrack] = useCurrentTrack()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const isBrowser = () => typeof window !== 'undefined'
|
const isBrowser = () => typeof window !== 'undefined'
|
||||||
@@ -145,6 +129,11 @@ export default function Teks({parole}) {
|
|||||||
window.scrollTo({top: 0, behavior: 'smooth'})
|
window.scrollTo({top: 0, behavior: 'smooth'})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentTrack(parole)
|
||||||
|
return () => setCurrentTrack(null)
|
||||||
|
}, [parole, setCurrentTrack])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Root className={classes.container}>
|
<Root className={classes.container}>
|
||||||
<Box sx={{textAlign: 'center', marginTop: 12}}>
|
<Box sx={{textAlign: 'center', marginTop: 12}}>
|
||||||
@@ -194,7 +183,7 @@ export default function Teks({parole}) {
|
|||||||
<i>parole soumise par {parole.user.username}</i>
|
<i>parole soumise par {parole.user.username}</i>
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
{parole?.userAdmin&& !parole.user && (
|
{parole?.userAdmin && !parole.user && (
|
||||||
<Typography style={{marginBottom: '1.5em'}} display='block' variant='caption'>
|
<Typography style={{marginBottom: '1.5em'}} display='block' variant='caption'>
|
||||||
<i>parole soumise par {parole.userAdmin.firstname}</i>
|
<i>parole soumise par {parole.userAdmin.firstname}</i>
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -204,8 +193,15 @@ export default function Teks({parole}) {
|
|||||||
<LicenseModal license={parole.creativeCommons.toLowerCase()} sourceOriginale={parole.sourceOriginale ?? null} remixes={parole.remixes?.length ? parole.remixes : null} />
|
<LicenseModal license={parole.creativeCommons.toLowerCase()} sourceOriginale={parole.sourceOriginale ?? null} remixes={parole.remixes?.length ? parole.remixes : null} />
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
{parole?.files && (
|
{(parole?.files || (parole.creativeCommons && parole.kits?.length > 0)) && (
|
||||||
<FilesDialog files={parole.files} />
|
<Box sx={{display: 'flex', flexWrap: 'wrap', justifyContent: 'center', gap: 1}}>
|
||||||
|
{parole?.files && (
|
||||||
|
<FilesDialog files={parole.files} />
|
||||||
|
)}
|
||||||
|
{parole.creativeCommons && parole.kits?.length > 0 && (
|
||||||
|
<KitsDialog kits={parole.kits} />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
)}
|
)}
|
||||||
{(parole.okiMizikID || parole.streamAudio.length > 0 || parole.gadeEmbed) && (
|
{(parole.okiMizikID || parole.streamAudio.length > 0 || parole.gadeEmbed) && (
|
||||||
<Box sx={{textAlign: 'center'}}>
|
<Box sx={{textAlign: 'center'}}>
|
||||||
@@ -224,42 +220,67 @@ export default function Teks({parole}) {
|
|||||||
Écouter sur
|
Écouter sur
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box sx={{display: 'flex', flexWrap: 'wrap', justifyContent: 'center', gap: 1}}>
|
<Box sx={{display: 'flex', flexWrap: 'wrap', justifyContent: 'center', gap: 1}}>
|
||||||
{parole.streamAudio.map((lyen, i) => (
|
{parole.streamAudio.map(lyen => (
|
||||||
<StreamButton key={i} lyen={lyen} />
|
<StreamButton key={lyen.url} lyen={lyen} />
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Grid container justifycontent='center' spacing={1}>
|
<Box sx={{maxWidth: 700, margin: '0 auto'}} className={classes.gridText}>
|
||||||
<Grid size={{xs: 12, md: langArray.length > 0 ? 6 : null}}>
|
<Tabs
|
||||||
<Box className={classes.gridText}>
|
scrollButtons
|
||||||
<Typography align='center' sx={{marginBottom: '0.5em'}} variant='h4'>
|
allowScrollButtonsMobile
|
||||||
Transcription
|
value={tab}
|
||||||
{parole.langueSource && parole.langueSource !== 'ka' && (
|
variant='scrollable'
|
||||||
<Typography component='span' variant='body2' color='text.secondary' sx={{ml: 1}}>
|
centered={langArray.length === 0}
|
||||||
({LANG_NAMES[parole.langueSource]})
|
slots={{
|
||||||
</Typography>
|
startScrollButtonIcon: ArrowBackIosNewIcon,
|
||||||
)}
|
endScrollButtonIcon: ArrowForwardIosIcon
|
||||||
</Typography>
|
}}
|
||||||
<Typography paragraph='true' align={alignTeks(langArray, isMobile)} component='span'>
|
sx={{
|
||||||
|
borderBottom: 1,
|
||||||
|
borderColor: 'divider',
|
||||||
|
mb: 2,
|
||||||
|
'& .MuiTabs-scrollButtons': {
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
borderRadius: '50%',
|
||||||
|
color: 'primary.main',
|
||||||
|
backgroundColor: 'action.hover',
|
||||||
|
mx: 0.5,
|
||||||
|
'&.Mui-disabled': {
|
||||||
|
opacity: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onChange={(event, value) => setTab(value)}
|
||||||
|
>
|
||||||
|
<Tab label='Transcription' />
|
||||||
|
{langArray.map(({title}) => (
|
||||||
|
<Tab key={title} label={title} />
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
{tab === 0 && (
|
||||||
|
<>
|
||||||
|
{parole.langueSource && parole.langueSource !== 'ka' && (
|
||||||
|
<Typography align='center' variant='body2' color='text.secondary' sx={{mb: 1}}>
|
||||||
|
({LANG_NAMES[parole.langueSource]})
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
<Typography paragraph='true' align={isMobile ? 'justify' : 'center'} component='span'>
|
||||||
{formatJsonString(parole.transcription)}
|
{formatJsonString(parole.transcription)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</>
|
||||||
</Grid>
|
)}
|
||||||
{langArray.map(({title, lang}) => (
|
{langArray.map(({title, lang}, index) => (
|
||||||
<Grid key={title} size={{xs: 12, md: 6}}>
|
tab === index + 1 && (
|
||||||
<Box className={classes.gridText}>
|
<Typography key={title} paragraph='true' align='justify' component='span'>
|
||||||
<Typography align='center' sx={{marginBottom: '0.5em'}} variant='h4'>
|
{formatJsonString(lang)}
|
||||||
{title}
|
</Typography>
|
||||||
</Typography>
|
)
|
||||||
<Typography paragraph='true' align='justify' component='span'>
|
|
||||||
{formatJsonString(lang)}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</Grid>
|
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Box>
|
||||||
</Root>
|
</Root>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
import NextTopLoader from 'nextjs-toploader'
|
import NextTopLoader from 'nextjs-toploader'
|
||||||
|
|
||||||
export default function TopLoader({color}) {
|
export default function TopLoader({color}) {
|
||||||
return <NextTopLoader color={color} />
|
return <NextTopLoader color={color} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TopLoader.propTypes = {
|
||||||
|
color: PropTypes.string.isRequired
|
||||||
|
}
|
||||||
|
|||||||
+14
-14
@@ -1,25 +1,25 @@
|
|||||||
import { dirname } from "path";
|
import {dirname} from 'node:path'
|
||||||
import { fileURLToPath } from "url";
|
import {fileURLToPath} from 'node:url'
|
||||||
import { FlatCompat } from "@eslint/eslintrc";
|
import {FlatCompat} from '@eslint/eslintrc'
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url)
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename)
|
||||||
|
|
||||||
const compat = new FlatCompat({
|
const compat = new FlatCompat({
|
||||||
baseDirectory: __dirname,
|
baseDirectory: __dirname,
|
||||||
});
|
})
|
||||||
|
|
||||||
const eslintConfig = [
|
const eslintConfig = [
|
||||||
...compat.extends("next/core-web-vitals"),
|
...compat.extends('next/core-web-vitals'),
|
||||||
{
|
{
|
||||||
ignores: [
|
ignores: [
|
||||||
"node_modules/**",
|
'node_modules/**',
|
||||||
".next/**",
|
'.next/**',
|
||||||
"out/**",
|
'out/**',
|
||||||
"build/**",
|
'build/**',
|
||||||
"next-env.d.ts",
|
'next-env.d.ts',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
]
|
||||||
|
|
||||||
export default eslintConfig;
|
export default eslintConfig
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import {describe, it, expect} from 'vitest'
|
||||||
|
import {formatKuveti} from '../kuveti'
|
||||||
|
|
||||||
|
const kuveti = {
|
||||||
|
url: '/uploads/original.jpg',
|
||||||
|
formats: {
|
||||||
|
thumbnail: {url: '/uploads/thumbnail.jpg', width: 156, height: 156},
|
||||||
|
small: {url: '/uploads/small.jpg', width: 500, height: 500},
|
||||||
|
medium: {url: '/uploads/medium.jpg', width: 750, height: 750},
|
||||||
|
large: {url: '/uploads/large.jpg', width: 1000, height: 1000},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('formatKuveti', () => {
|
||||||
|
it('returns null when there is no image', () => {
|
||||||
|
expect(formatKuveti(null)).toBeNull()
|
||||||
|
expect(formatKuveti(undefined)).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('defaults to the large format', () => {
|
||||||
|
expect(formatKuveti(kuveti)).toBe(kuveti.formats.large)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('prefers the thumbnail format when asked', () => {
|
||||||
|
expect(formatKuveti(kuveti, 'thumbnail')).toBe(kuveti.formats.thumbnail)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('falls back to small then medium then large when thumbnail is missing', () => {
|
||||||
|
const {thumbnail, ...rest} = kuveti.formats
|
||||||
|
const withoutThumbnail = {...kuveti, formats: rest}
|
||||||
|
expect(formatKuveti(withoutThumbnail, 'thumbnail')).toBe(rest.small)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('falls back to the original file when no format matches', () => {
|
||||||
|
const noFormats = {url: '/uploads/original.jpg', formats: {}}
|
||||||
|
expect(formatKuveti(noFormats, 'thumbnail')).toBe(noFormats)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import {describe, it, expect, vi, afterEach} from 'vitest'
|
||||||
|
import {jwennTeksSlug} from '../oki-api'
|
||||||
|
|
||||||
|
function fakeEntry(i) {
|
||||||
|
return {slug: `slug-${i}`}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('jwennTeksSlug (full pagination)', () => {
|
||||||
|
const originalFetch = global.fetch
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
global.fetch = originalFetch
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('walks every page instead of stopping at the first pageSize chunk', async () => {
|
||||||
|
const pageSize = 200
|
||||||
|
const totalRecords = 250
|
||||||
|
|
||||||
|
let calls = 0
|
||||||
|
global.fetch = vi.fn(async url => {
|
||||||
|
calls++
|
||||||
|
if (calls > 5) {
|
||||||
|
throw new Error('too many pagination calls, likely an infinite loop')
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageMatch = url.match(/pagination\[page]=(\d+)/)
|
||||||
|
const page = pageMatch ? Number(pageMatch[1]) : 1
|
||||||
|
const start = (page - 1) * pageSize
|
||||||
|
const end = Math.min(start + pageSize, totalRecords)
|
||||||
|
const data = Array.from({length: Math.max(end - start, 0)}, (_, i) => fakeEntry(start + i))
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({data})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const slugs = await jwennTeksSlug()
|
||||||
|
|
||||||
|
expect(slugs).toHaveLength(totalRecords)
|
||||||
|
expect(slugs[0]).toBe('slug-0')
|
||||||
|
expect(slugs.at(-1)).toBe('slug-249')
|
||||||
|
expect(global.fetch).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('stops after a single page when there are fewer records than pageSize', async () => {
|
||||||
|
global.fetch = vi.fn(async () => ({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({data: [fakeEntry(1), fakeEntry(2)]})
|
||||||
|
}))
|
||||||
|
|
||||||
|
const slugs = await jwennTeksSlug()
|
||||||
|
|
||||||
|
expect(slugs).toEqual(['slug-1', 'slug-2'])
|
||||||
|
expect(global.fetch).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import {describe, it, expect, vi, afterEach} from 'vitest'
|
||||||
|
import {jwennAnTeks} from '../oki-api'
|
||||||
|
|
||||||
|
describe('request (via jwennAnTeks)', () => {
|
||||||
|
const originalFetch = global.fetch
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
global.fetch = originalFetch
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns the parsed body on a successful response', async () => {
|
||||||
|
global.fetch = vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({id: 1, titre: 'Test'})
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await jwennAnTeks(1, 'token')
|
||||||
|
expect(result).toEqual({id: 1, titre: 'Test'})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws a clear error when Strapi returns an error response', async () => {
|
||||||
|
global.fetch = vi.fn().mockResolvedValue({
|
||||||
|
ok: false,
|
||||||
|
status: 400,
|
||||||
|
json: async () => ({data: null, error: {status: 400, name: 'ValidationError', message: 'Invalid key'}})
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(jwennAnTeks(1, 'token')).rejects.toThrow('Eshwe pu jwenn done-a')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws when the network request itself fails', async () => {
|
||||||
|
global.fetch = vi.fn().mockRejectedValue(new TypeError('network down'))
|
||||||
|
|
||||||
|
await expect(jwennAnTeks(1, 'token')).rejects.toThrow('Eshwe pu jwenn done-a')
|
||||||
|
})
|
||||||
|
})
|
||||||
+11
-4
@@ -1,14 +1,21 @@
|
|||||||
const SIZE_ORDER = {
|
const SIZE_ORDER = {
|
||||||
large: ['large', 'medium', 'small'],
|
large: ['large', 'medium', 'small'],
|
||||||
medium: ['medium', 'small', 'large'],
|
medium: ['medium', 'small', 'large'],
|
||||||
small: ['small', 'medium', 'large'],
|
small: ['small', 'medium', 'large'],
|
||||||
|
thumbnail: ['thumbnail', 'small', 'medium', 'large'],
|
||||||
}
|
}
|
||||||
|
|
||||||
export const formatKuveti = (kuveti, preferred = 'large') => {
|
export const formatKuveti = (kuveti, preferred = 'large') => {
|
||||||
if (!kuveti) return null
|
if (!kuveti) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
const order = SIZE_ORDER[preferred] ?? SIZE_ORDER.large
|
const order = SIZE_ORDER[preferred] ?? SIZE_ORDER.large
|
||||||
for (const size of order) {
|
for (const size of order) {
|
||||||
if (kuveti.formats?.[size]) return kuveti.formats[size]
|
if (kuveti.formats?.[size]) {
|
||||||
|
return kuveti.formats[size]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return kuveti
|
return kuveti
|
||||||
}
|
}
|
||||||
|
|||||||
+43
-52
@@ -14,12 +14,18 @@ const headers = {
|
|||||||
async function request(url, opts = {}) {
|
async function request(url, opts = {}) {
|
||||||
const {...options} = opts
|
const {...options} = opts
|
||||||
|
|
||||||
|
let res
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${OKI_API}${url}`, options)
|
res = await fetch(`${OKI_API}${url}`, options)
|
||||||
return res.json()
|
|
||||||
} catch {
|
} catch {
|
||||||
throw new Error('Eshwe pu jwenn done-a')
|
throw new Error('Eshwe pu jwenn done-a')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error('Eshwe pu jwenn done-a')
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function jwennTeksEpiSlug(slug) {
|
export async function jwennTeksEpiSlug(slug) {
|
||||||
@@ -54,6 +60,9 @@ export async function jwennTeksEpiSlug(slug) {
|
|||||||
streamVideo: {
|
streamVideo: {
|
||||||
populate: '*'
|
populate: '*'
|
||||||
},
|
},
|
||||||
|
kits: {
|
||||||
|
populate: '*'
|
||||||
|
},
|
||||||
traductions: {
|
traductions: {
|
||||||
populate: '*'
|
populate: '*'
|
||||||
},
|
},
|
||||||
@@ -92,7 +101,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', 'titrePhare', 'titrePhare.streamAudio', 'titrePhare.streamVideo', 'titrePhare.couverture'],
|
populate: ['paroles', 'photo', 'paroles.couverture', 'titrePhare', 'titrePhare.streamAudio', 'titrePhare.streamVideo', 'titrePhare.couverture', 'rezoSosyal'],
|
||||||
filters: {
|
filters: {
|
||||||
slug: {
|
slug: {
|
||||||
$eq: slug
|
$eq: slug
|
||||||
@@ -130,7 +139,7 @@ export async function jwennToutAwtis() {
|
|||||||
populate: ['photo'],
|
populate: ['photo'],
|
||||||
sort: ['alias:asc'],
|
sort: ['alias:asc'],
|
||||||
pagination: {
|
pagination: {
|
||||||
pageSize: 100
|
pageSize: 200
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
encodeValuesOnly: true
|
encodeValuesOnly: true
|
||||||
@@ -143,39 +152,16 @@ export async function jwennToutAwtis() {
|
|||||||
export async function jwennTeks() {
|
export async function jwennTeks() {
|
||||||
const query = qs.stringify({
|
const query = qs.stringify({
|
||||||
populate: {
|
populate: {
|
||||||
user: {
|
|
||||||
fields: ['username']
|
|
||||||
},
|
|
||||||
userAdmin: {
|
|
||||||
fields: ['username', 'firstname']
|
|
||||||
},
|
|
||||||
commentaires: {
|
|
||||||
fields: ['contenu', 'datePublication'],
|
|
||||||
populate: {
|
|
||||||
user: {
|
|
||||||
fields: ['username']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
couverture: {
|
couverture: {
|
||||||
populate: '*'
|
populate: '*'
|
||||||
},
|
},
|
||||||
artistes: {
|
artistes: {
|
||||||
fields: ['alias', 'slug']
|
fields: ['alias', 'slug']
|
||||||
},
|
|
||||||
streamAudio: {
|
|
||||||
populate: '*'
|
|
||||||
},
|
|
||||||
streamVideo: {
|
|
||||||
populate: '*'
|
|
||||||
},
|
|
||||||
traductions: {
|
|
||||||
populate: '*'
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
sort: ['titre:asc'],
|
sort: ['titre:asc'],
|
||||||
pagination: {
|
pagination: {
|
||||||
pageSize: 100
|
pageSize: 200
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
encodeValuesOnly: true
|
encodeValuesOnly: true
|
||||||
@@ -185,32 +171,37 @@ export async function jwennTeks() {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function jwennAwtisSlug() {
|
async function jwennTouSlug(endpoint) {
|
||||||
const query = qs.stringify({
|
const pageSize = 200
|
||||||
sort: ['createdAt:desc'],
|
const slugs = []
|
||||||
pagination: {
|
let page = 1
|
||||||
pageSize: 100
|
let hasMore = true
|
||||||
}
|
|
||||||
}, {
|
|
||||||
encodeValuesOnly: true
|
|
||||||
})
|
|
||||||
|
|
||||||
const {data} = await request(`/artistes?${query}`, headers)
|
while (hasMore) {
|
||||||
return data.map(({slug}) => slug)
|
const query = qs.stringify({
|
||||||
|
sort: ['createdAt:desc'],
|
||||||
|
fields: ['slug'],
|
||||||
|
pagination: {page, pageSize}
|
||||||
|
}, {
|
||||||
|
encodeValuesOnly: true
|
||||||
|
})
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-await-in-loop -- pagination séquentielle, chaque page dépend du nombre de résultats de la précédente
|
||||||
|
const {data} = await request(`/${endpoint}?${query}`, headers)
|
||||||
|
slugs.push(...data.map(({slug}) => slug))
|
||||||
|
hasMore = data.length === pageSize
|
||||||
|
page += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return slugs
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function jwennAwtisSlug() {
|
||||||
|
return jwennTouSlug('artistes')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function jwennTeksSlug() {
|
export async function jwennTeksSlug() {
|
||||||
const query = qs.stringify({
|
return jwennTouSlug('paroles')
|
||||||
sort: ['createdAt:desc'],
|
|
||||||
pagination: {
|
|
||||||
pageSize: 100
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
encodeValuesOnly: true
|
|
||||||
})
|
|
||||||
|
|
||||||
const {data} = await request(`/paroles?${query}`, headers)
|
|
||||||
return data.map(({slug}) => slug)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function jwennDenyeTeks() {
|
export async function jwennDenyeTeks() {
|
||||||
@@ -233,9 +224,9 @@ export async function jwennAnVedette() {
|
|||||||
const query = qs.stringify({
|
const query = qs.stringify({
|
||||||
populate: ['artistes', 'couverture'],
|
populate: ['artistes', 'couverture'],
|
||||||
filters: {
|
filters: {
|
||||||
isNewRelease: { $eq: true }
|
isNewRelease: {$eq: true}
|
||||||
},
|
},
|
||||||
pagination: { limit: 1 }
|
pagination: {limit: 1}
|
||||||
}, {
|
}, {
|
||||||
encodeValuesOnly: true
|
encodeValuesOnly: true
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
import YouTubeIcon from '@mui/icons-material/YouTube'
|
import YouTubeIcon from '@mui/icons-material/YouTube'
|
||||||
import TelegramIcon from '@mui/icons-material/Telegram'
|
import TelegramIcon from '@mui/icons-material/Telegram'
|
||||||
|
|
||||||
import PeertubeIcon from './icons/peertube'
|
|
||||||
import XmppIcon from './icons/xmpp'
|
|
||||||
import XIcon from './icons/x'
|
import XIcon from './icons/x'
|
||||||
import GiteaIcon from './icons/gitea'
|
import GiteaIcon from './icons/gitea'
|
||||||
import BlueskyIcon from './icons/bluesky'
|
import BlueskyIcon from './icons/bluesky'
|
||||||
|
|
||||||
const gitURL = process.env.NEXT_PUBLIC_GIT || 'https://labola.o-k-i.net/ORGANISATION-KA-INTERNATIONALE/pawol.nu'
|
const gitURL = process.env.NEXT_PUBLIC_GIT || 'https://labola.o-k-i.net/ORGANISATION-KA-INTERNATIONALE/pawol.nu'
|
||||||
const blueskyUrl = process.env.NEXT_PUBLIC_BLUESKY_URL || 'https://bsky.app/profile/organisationka.bsky.social'
|
const blueskyUrl = process.env.NEXT_PUBLIC_BLUESKY_URL || 'https://bsky.app/profile/organisationka.bsky.social'
|
||||||
const xmppContact = process.env.NEXT_PUBLIC_XMPP || 'oki@xmpp.cz'
|
|
||||||
const gadeUsername = process.env.NEXT_PUBLIC_GADE_USERNAME || 'oki'
|
|
||||||
const youtubeUsername = process.env.NEXT_PUBLIC_YOUTUBE_USERNAME || 'OrganisationKInternationaleOKi'
|
const youtubeUsername = process.env.NEXT_PUBLIC_YOUTUBE_USERNAME || 'OrganisationKInternationaleOKi'
|
||||||
const tiwtterUsername = process.env.NEXT_PUBLIC_TWITTER_USERNAME || 'OrganisationKA'
|
const tiwtterUsername = process.env.NEXT_PUBLIC_TWITTER_USERNAME || 'OrganisationKA'
|
||||||
const telegramGroup = process.env.NEXT_PUBLIC_TELEGRAM_GROUP || 'OrganisationKA'
|
const telegramGroup = process.env.NEXT_PUBLIC_TELEGRAM_GROUP || 'OrganisationKA'
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import {describe, it, expect} from 'vitest'
|
||||||
|
import {getAlias} from '../get-alias'
|
||||||
|
|
||||||
|
describe('getAlias', () => {
|
||||||
|
it('retourne une chaîne vide sans planter quand artistes est undefined', () => {
|
||||||
|
expect(getAlias(undefined)).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('retourne une chaîne vide sans planter quand artistes est un tableau vide', () => {
|
||||||
|
expect(getAlias([])).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('retourne l\'alias unique quand il n\'y a qu\'un artiste', () => {
|
||||||
|
expect(getAlias([{id: 1, alias: 'Foo'}])).toBe('Foo')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ignore un id de priorité qui ne correspond à aucun artiste (donnée obsolète)', () => {
|
||||||
|
const artistes = [{id: 1, alias: 'Foo'}, {id: 2, alias: 'Bar'}]
|
||||||
|
expect(getAlias(artistes, '1-99-2')).toBe('Foo et Bar')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('formate plusieurs artistes selon la priorité fournie', () => {
|
||||||
|
const artistes = [{id: 1, alias: 'Foo'}, {id: 2, alias: 'Bar'}]
|
||||||
|
expect(getAlias(artistes, '2-1')).toBe('Bar et Foo')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,54 +1,2 @@
|
|||||||
import slugify from 'slugify'
|
|
||||||
|
|
||||||
export const formatJsonString = stringToFormat =>
|
export const formatJsonString = stringToFormat =>
|
||||||
stringToFormat.split('\n').map((string, index) => <div key={index}>{`${string}`}<br /></div>) // eslint-disable-line react/no-array-index-key
|
stringToFormat.split('\n').map((string, index) => <div key={index}>{`${string}`}<br /></div>) // eslint-disable-line react/no-array-index-key
|
||||||
|
|
||||||
export const getAlias = (artistes, priorite = null, enhanced = false) => {
|
|
||||||
if (artistes.length === 1 && !enhanced) {
|
|
||||||
return artistes[0].alias
|
|
||||||
}
|
|
||||||
|
|
||||||
if (artistes.length === 1 && enhanced) {
|
|
||||||
return [{
|
|
||||||
type: 'element',
|
|
||||||
value: artistes[0].alias
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
|
|
||||||
const alias = []
|
|
||||||
const splittedPriority = priorite ? priorite.split('-')
|
|
||||||
.map(p => Number.parseInt(p, 10)) : null
|
|
||||||
|
|
||||||
if (splittedPriority) {
|
|
||||||
for (const element of splittedPriority) {
|
|
||||||
const foundedArtistes = artistes.find(({id}) => id === element)
|
|
||||||
alias.push(foundedArtistes.alias)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (const artiste of artistes) {
|
|
||||||
alias.push(artiste.alias)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enhanced) {
|
|
||||||
const list = new Intl.ListFormat('fr').formatToParts(alias)
|
|
||||||
return list
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Intl.ListFormat('fr').format(alias)
|
|
||||||
}
|
|
||||||
|
|
||||||
function separateString(inputString) {
|
|
||||||
const regex = /\s*,\s*|\s+et\s+/
|
|
||||||
const strings = inputString.split(regex).map(element => element.trim())
|
|
||||||
return strings
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getAliasObject = inputString => {
|
|
||||||
const awtis = separateString(inputString)
|
|
||||||
|
|
||||||
return awtis.map(alias => ({
|
|
||||||
alias,
|
|
||||||
slug: slugify(alias, {lower: true, remove: /[*#+~.()'"!:@]/g})
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import slugify from 'slugify'
|
||||||
|
|
||||||
|
export const getAlias = (artistes, priorite = null, enhanced = false) => {
|
||||||
|
const safeArtistes = artistes || []
|
||||||
|
|
||||||
|
if (safeArtistes.length === 1 && !enhanced) {
|
||||||
|
return safeArtistes[0].alias
|
||||||
|
}
|
||||||
|
|
||||||
|
if (safeArtistes.length === 1 && enhanced) {
|
||||||
|
return [{
|
||||||
|
type: 'element',
|
||||||
|
value: safeArtistes[0].alias
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
const alias = []
|
||||||
|
const splittedPriority = priorite ? priorite.split('-')
|
||||||
|
.map(p => Number.parseInt(p, 10)) : null
|
||||||
|
|
||||||
|
if (splittedPriority) {
|
||||||
|
for (const element of splittedPriority) {
|
||||||
|
const foundedArtistes = safeArtistes.find(({id}) => id === element)
|
||||||
|
if (foundedArtistes) {
|
||||||
|
alias.push(foundedArtistes.alias)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const artiste of safeArtistes) {
|
||||||
|
alias.push(artiste.alias)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enhanced) {
|
||||||
|
const list = new Intl.ListFormat('fr').formatToParts(alias)
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Intl.ListFormat('fr').format(alias)
|
||||||
|
}
|
||||||
|
|
||||||
|
function separateString(inputString) {
|
||||||
|
const regex = /\s*,\s*|\s+et\s+/
|
||||||
|
const strings = inputString.split(regex).map(element => element.trim())
|
||||||
|
return strings
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAliasObject = inputString => {
|
||||||
|
const awtis = separateString(inputString)
|
||||||
|
|
||||||
|
return awtis.map(alias => ({
|
||||||
|
alias,
|
||||||
|
slug: slugify(alias, {lower: true, remove: /[*#+~.()'"!:@]/g})
|
||||||
|
}))
|
||||||
|
}
|
||||||
+10
-11
@@ -11,26 +11,25 @@ function buildRemotePatterns() {
|
|||||||
const [hostname, port] = entry.split(':')
|
const [hostname, port] = entry.split(':')
|
||||||
const isLocal = hostname === 'localhost' || hostname === '127.0.0.1'
|
const isLocal = hostname === 'localhost' || hostname === '127.0.0.1'
|
||||||
const pattern = {protocol: isLocal ? 'http' : 'https', hostname, pathname: '/uploads/**'}
|
const pattern = {protocol: isLocal ? 'http' : 'https', hostname, pathname: '/uploads/**'}
|
||||||
if (port) pattern.port = port
|
if (port) {
|
||||||
|
pattern.port = port
|
||||||
|
}
|
||||||
|
|
||||||
return pattern
|
return pattern
|
||||||
})
|
})
|
||||||
if (!raw.includes('localhost'))
|
if (!raw.includes('localhost')) {
|
||||||
patterns.push({protocol: 'http', hostname: 'localhost', port: '1337', pathname: '/uploads/**'})
|
patterns.push({protocol: 'http', hostname: 'localhost', port: '1337', pathname: '/uploads/**'})
|
||||||
if (!raw.includes('127.0.0.1'))
|
}
|
||||||
|
|
||||||
|
if (!raw.includes('127.0.0.1')) {
|
||||||
patterns.push({protocol: 'http', hostname: '127.0.0.1', port: '1337', pathname: '/uploads/**'})
|
patterns.push({protocol: 'http', hostname: '127.0.0.1', port: '1337', pathname: '/uploads/**'})
|
||||||
|
}
|
||||||
|
|
||||||
return patterns
|
return patterns
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = (withPWA({
|
module.exports = (withPWA({
|
||||||
turbopack: {},
|
turbopack: {},
|
||||||
webpack: config => {
|
|
||||||
config.module.rules.push({
|
|
||||||
test: /\.svg$/,
|
|
||||||
use: ['@svgr/webpack', 'url-loader'],
|
|
||||||
})
|
|
||||||
|
|
||||||
return config
|
|
||||||
},
|
|
||||||
images: {
|
images: {
|
||||||
remotePatterns: buildRemotePatterns()
|
remotePatterns: buildRemotePatterns()
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-14
@@ -17,7 +17,9 @@
|
|||||||
"lint": "xo",
|
"lint": "xo",
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start"
|
"start": "next start",
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/cache": "^11.11.0",
|
"@emotion/cache": "^11.11.0",
|
||||||
@@ -32,36 +34,35 @@
|
|||||||
"@mui/material-pigment-css": "^9.0.0",
|
"@mui/material-pigment-css": "^9.0.0",
|
||||||
"@mui/styles": "^6.4.8",
|
"@mui/styles": "^6.4.8",
|
||||||
"@pigment-css/react": "^0.0.30",
|
"@pigment-css/react": "^0.0.30",
|
||||||
"@svgr/webpack": "^6.5.0",
|
|
||||||
"axios": "^0.21.0",
|
|
||||||
"compression": "^1.7.4",
|
|
||||||
"date-fns": "^2.16.1",
|
"date-fns": "^2.16.1",
|
||||||
"dotenv": "^16.0.0",
|
"dotenv": "^16.0.0",
|
||||||
"express": "^4.17.1",
|
|
||||||
"file-saver": "^2.0.5",
|
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"music-metadata-browser": "^2.5.11",
|
"music-metadata-browser": "^2.5.11",
|
||||||
"next": "16.2.4",
|
"next": "16.2.4",
|
||||||
"next-auth": "^5.0.0-beta.31",
|
|
||||||
"next-plausible": "^4.0.0",
|
"next-plausible": "^4.0.0",
|
||||||
"next-pwa": "5.6.0",
|
"next-pwa": "5.6.0",
|
||||||
"nextjs-toploader": "^3.9.17",
|
"nextjs-toploader": "^3.9.17",
|
||||||
"nodemailer": "^6.7.2",
|
"nodemailer": "^6.7.2",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
|
"qs": "^6.15.3",
|
||||||
"react": "19.2.5",
|
"react": "19.2.5",
|
||||||
"react-dom": "19.2.5",
|
"react-dom": "19.2.5",
|
||||||
"react-swipeable-views": "^0.13.9",
|
|
||||||
"react-virtuoso": "^4.4.0",
|
"react-virtuoso": "^4.4.0",
|
||||||
"sharp": "^0.29.3",
|
"sharp": "^0.29.3",
|
||||||
"slugify": "^1.6.6"
|
"slugify": "^1.6.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
|
"@testing-library/react": "^16.3.2",
|
||||||
|
"@testing-library/user-event": "^14.6.1",
|
||||||
|
"@vitejs/plugin-react": "^6.0.3",
|
||||||
"eslint": "^8.7.0",
|
"eslint": "^8.7.0",
|
||||||
"eslint-config-next": "16.2.4",
|
|
||||||
"eslint-config-xo-nextjs": "^4.0.3",
|
"eslint-config-xo-nextjs": "^4.0.3",
|
||||||
"eslint-plugin-react": "^7.28.0",
|
"eslint-plugin-react": "^7.28.0",
|
||||||
"eslint-plugin-react-hooks": "^4.3.0",
|
"eslint-plugin-react-hooks": "^4.3.0",
|
||||||
|
"jsdom": "^29.1.1",
|
||||||
|
"vitest": "^4.1.9",
|
||||||
"xo": "^0.47.0"
|
"xo": "^0.47.0"
|
||||||
},
|
},
|
||||||
"xo": {
|
"xo": {
|
||||||
@@ -83,11 +84,11 @@
|
|||||||
"error",
|
"error",
|
||||||
{
|
{
|
||||||
"capIsNewExceptions": [
|
"capIsNewExceptions": [
|
||||||
"CredentialsProvider",
|
"GET",
|
||||||
"TwitterProvider",
|
"POST",
|
||||||
"GoogleProvider",
|
"PUT",
|
||||||
"GitHubProvider",
|
"DELETE",
|
||||||
"NextAuth"
|
"PATCH"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
Binary file not shown.
@@ -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*
|
||||||
Executable
+63
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||||
|
.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")"
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import {defineConfig} from 'vitest/config'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
test: {
|
||||||
|
environment: 'jsdom',
|
||||||
|
setupFiles: ['./vitest.setup.js'],
|
||||||
|
globals: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import '@testing-library/jest-dom/vitest' // eslint-disable-line import/no-unassigned-import
|
||||||
Reference in New Issue
Block a user