Compare commits
31
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5936de59ff
|
||
|
|
6804e3626f
|
||
|
|
461b063cff
|
||
|
|
6601b00545
|
||
|
|
f88b0184ea
|
||
|
|
43964ec91d
|
||
|
|
ce19fa3b5d
|
||
|
|
33514bf633
|
||
|
|
35c39a97f3
|
||
|
|
72b86fdc5c
|
||
|
|
53287f9c80
|
||
|
|
8979970a90
|
||
|
|
75c5979478
|
||
|
|
5778d865e2
|
||
|
|
0ac18faf5c | ||
|
|
21248b7138
|
||
|
|
eda30345c6
|
||
|
|
b40e2d71b0
|
||
|
|
4bc545ac72
|
||
|
|
64cdd784c3
|
||
|
|
d2d09c28b3 | ||
|
|
eae8bb03a9
|
||
|
|
454eefbe58
|
||
|
|
b193914c57
|
||
|
|
d700ee6def
|
||
|
|
2132df3e0c
|
||
|
|
c7e3faf10a
|
||
|
|
4c662e89b5
|
||
|
|
2bec90210a
|
||
|
|
44e1a426b0
|
||
|
|
d2971870c1
|
+6
-6
@@ -3,8 +3,7 @@ API_URL=http://localhost:1337/api
|
|||||||
NEXT_PUBLIC_API_URL=$API_URL
|
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=
|
API_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.
|
||||||
@@ -77,18 +76,19 @@ 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=
|
||||||
NEXT_PUBLIC_BLUESKY_URL=
|
NEXT_PUBLIC_BLUESKY_URL=
|
||||||
|
|
||||||
|
# Commentaires fédérés (bokante.o-k-i.net)
|
||||||
|
|
||||||
|
NEXT_PUBLIC_BOKANTE_URL=https://bokante.o-k-i.net
|
||||||
|
NEXT_PUBLIC_BOKANTE_ACCOUNT=pawol_nu
|
||||||
|
|
||||||
# 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
|
||||||
@@ -33,6 +39,7 @@ jobs:
|
|||||||
username: ${{ secrets.SSH_USER }}
|
username: ${{ secrets.SSH_USER }}
|
||||||
key: ${{ secrets.SSH_KEY }}
|
key: ${{ secrets.SSH_KEY }}
|
||||||
script: |
|
script: |
|
||||||
|
set -e
|
||||||
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 }}
|
||||||
|
|||||||
@@ -20,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
|
||||||
@@ -31,6 +37,7 @@ jobs:
|
|||||||
username: ${{ secrets.SSH_USER }}
|
username: ${{ secrets.SSH_USER }}
|
||||||
key: ${{ secrets.SSH_KEY }}
|
key: ${{ secrets.SSH_KEY }}
|
||||||
script: |
|
script: |
|
||||||
|
set -e
|
||||||
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 }}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -33,6 +39,7 @@ jobs:
|
|||||||
username: ${{ secrets.SSH_USER }}
|
username: ${{ secrets.SSH_USER }}
|
||||||
key: ${{ secrets.SSH_KEY }}
|
key: ${{ secrets.SSH_KEY }}
|
||||||
script: |
|
script: |
|
||||||
|
set -e
|
||||||
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.PROD_DEPLOY_PATH }}
|
cd ${{ secrets.PROD_DEPLOY_PATH }}
|
||||||
|
|||||||
@@ -24,6 +24,24 @@ cp .env.sample .env
|
|||||||
yarn && yarn dev
|
yarn && yarn dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Commentaires fédérés (Mastodon / bokante.o-k-i.net)
|
||||||
|
|
||||||
|
Les commentaires sous une parole peuvent provenir d'un compte pawol.nu ou du Fediverse
|
||||||
|
(réponses reçues sur le statut miroir publié sur bokante.o-k-i.net par le backend). Un
|
||||||
|
bouton permet de répondre directement depuis son propre compte Mastodon, quelle que soit
|
||||||
|
son instance, via `authorize_interaction` — aucun compte pawol.nu requis.
|
||||||
|
|
||||||
|
**Variables d'environnement :**
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
|---|---|
|
||||||
|
| `NEXT_PUBLIC_BOKANTE_URL` | Instance Mastodon de l'organisation (défaut : `https://bokante.o-k-i.net`) |
|
||||||
|
| `NEXT_PUBLIC_BOKANTE_ACCOUNT` | Compte bot dont les statuts servent de miroir (défaut : `pawol_nu`) |
|
||||||
|
|
||||||
|
Voir le RFC dédié pour le détail de la conception
|
||||||
|
(`RFC-commentaires-activitypub-2026-07-04.md`, à la racine du dossier `PAWOL.NU`) et le
|
||||||
|
`README.md` de `api.pawol.nu` pour la configuration côté backend.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright (C) 2020 - 2026 Cédric Famibelle-Pronzola & ORGANISATION KA INTERNATIONALE (OKI)
|
Copyright (C) 2020 - 2026 Cédric Famibelle-Pronzola & ORGANISATION KA INTERNATIONALE (OKI)
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import {describe, it, expect, vi, afterEach} from 'vitest'
|
||||||
|
import {jwennToutAwtis} from '../../../../lib/oki-api'
|
||||||
|
import {GET} from '../route'
|
||||||
|
|
||||||
|
vi.mock('../../../../lib/oki-api', () => ({
|
||||||
|
jwennToutAwtis: vi.fn()
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('GET /api/awtis', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renvoie les données de jwennToutAwtis en JSON, sans exposer de jeton', async () => {
|
||||||
|
jwennToutAwtis.mockResolvedValue({data: [{id: 1, alias: 'Foo'}], meta: {}})
|
||||||
|
|
||||||
|
const response = await GET()
|
||||||
|
const body = await response.json()
|
||||||
|
|
||||||
|
expect(body).toEqual({data: [{id: 1, alias: 'Foo'}], meta: {}})
|
||||||
|
expect(JSON.stringify(body)).not.toMatch(/bearer|token/i)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import {jwennToutAwtis} from '../../../lib/oki-api'
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const data = await jwennToutAwtis()
|
||||||
|
return Response.json(data)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
+3
-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,10 +7,11 @@ 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'
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
async function jwennDone() {
|
async function jwennDone() {
|
||||||
const [statistik, denyeTeks, anVedette] = await Promise.all([jwennStats(), jwennDenyeTeks(), jwennAnVedette()])
|
const [statistik, denyeTeks, anVedette] = await Promise.all([jwennStats(), jwennDenyeTeks(), jwennAnVedette()])
|
||||||
|
|
||||||
@@ -28,7 +27,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} />
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ 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)
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {Suspense} from 'react'
|
import {Suspense} from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
import TeksDrawerLoader from '../../components/teks/teks-drawer-loader'
|
import TeksDrawerLoader from '../../components/teks/teks-drawer-loader'
|
||||||
import {CurrentTrackProvider} from '../../components/teks/current-track-context'
|
import {CurrentTrackProvider} from '../../components/teks/current-track-context'
|
||||||
import Loading from './loading'
|
import Loading from './loading'
|
||||||
@@ -50,3 +51,7 @@ export default function PawolLayout({children}) {
|
|||||||
</CurrentTrackProvider>
|
</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.NODE_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}}>
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -72,7 +72,8 @@ export default function AwtisDetay({anAwtis}) {
|
|||||||
position: 'relative',
|
position: 'relative',
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
mb: 2,
|
mb: 2,
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
{photoUrl ? (
|
{photoUrl ? (
|
||||||
<Image
|
<Image
|
||||||
src={photoUrl}
|
src={photoUrl}
|
||||||
@@ -94,8 +95,8 @@ export default function AwtisDetay({anAwtis}) {
|
|||||||
|
|
||||||
{rezoSosyal?.length > 0 && (
|
{rezoSosyal?.length > 0 && (
|
||||||
<Box sx={{display: 'flex', gap: 0.75, flexWrap: 'wrap', justifyContent: 'center', mb: 1.5}}>
|
<Box sx={{display: 'flex', gap: 0.75, flexWrap: 'wrap', justifyContent: 'center', mb: 1.5}}>
|
||||||
{rezoSosyal.map((rezo, i) => (
|
{rezoSosyal.map(rezo => (
|
||||||
<SocialButton key={i} rezo={rezo} />
|
<SocialButton key={rezo.url} rezo={rezo} />
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
@@ -149,9 +150,9 @@ export default function AwtisDetay({anAwtis}) {
|
|||||||
<Link href={`/paroles/${titrePhare.slug}`} style={{display: 'block', height: '100%'}}>
|
<Link href={`/paroles/${titrePhare.slug}`} style={{display: 'block', height: '100%'}}>
|
||||||
<Box sx={{position: 'relative', minHeight: 130, height: '100%'}}>
|
<Box sx={{position: 'relative', minHeight: 130, height: '100%'}}>
|
||||||
<Image
|
<Image
|
||||||
|
fill
|
||||||
src={coverUrl}
|
src={coverUrl}
|
||||||
alt={titrePhare.titre}
|
alt={titrePhare.titre}
|
||||||
fill
|
|
||||||
placeholder='blur'
|
placeholder='blur'
|
||||||
blurDataURL={BLUR_DATA_URL}
|
blurDataURL={BLUR_DATA_URL}
|
||||||
sizes='200px'
|
sizes='200px'
|
||||||
@@ -163,7 +164,7 @@ export default function AwtisDetay({anAwtis}) {
|
|||||||
)}
|
)}
|
||||||
<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}}>
|
||||||
<Link href={`/paroles/${titrePhare.slug}`} style={{color: 'inherit', textDecoration: 'none'}}>
|
<Link href={`/paroles/${titrePhare.slug}`} style={{color: 'inherit', textDecoration: 'none'}}>
|
||||||
{titrePhare.titre}
|
{titrePhare.titre}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -172,8 +173,8 @@ export default function AwtisDetay({anAwtis}) {
|
|||||||
É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>
|
||||||
@@ -202,7 +203,7 @@ export default function AwtisDetay({anAwtis}) {
|
|||||||
})}
|
})}
|
||||||
</AccordionDetails>
|
</AccordionDetails>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
) : paroles.length === 0 ? (
|
) : (paroles.length === 0 ? (
|
||||||
<Typography textAlign='center' variant='body1' color='text.secondary'>
|
<Typography textAlign='center' variant='body1' color='text.secondary'>
|
||||||
Aucune parole pour le moment
|
Aucune parole pour le moment
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -213,7 +214,7 @@ export default function AwtisDetay({anAwtis}) {
|
|||||||
<MizikLyen anPawol={paroles[0]} kuveti={formatKuveti(paroles[0].couverture, 'thumbnail')} />
|
<MizikLyen anPawol={paroles[0]} kuveti={formatKuveti(paroles[0].couverture, 'thumbnail')} />
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ 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'
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ 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 {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'
|
||||||
@@ -28,7 +26,7 @@ export default function ChecheAwtis() {
|
|||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const {data} = await jwennToutAwtis()
|
const {data} = await fetch('/api/awtis').then(response => response.json())
|
||||||
|
|
||||||
const filteredData = data.map(artiste => {
|
const filteredData = data.map(artiste => {
|
||||||
const firstLetter = artiste.alias[0].toUpperCase()
|
const firstLetter = artiste.alias[0].toUpperCase()
|
||||||
@@ -50,15 +48,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
|
||||||
@@ -70,35 +66,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}${formatKuveti(option?.photo, 'thumbnail')?.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)}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
import Chip from '@mui/material/Chip'
|
import Chip from '@mui/material/Chip'
|
||||||
import IconButton from '@mui/material/IconButton'
|
import IconButton from '@mui/material/IconButton'
|
||||||
import Tooltip from '@mui/material/Tooltip'
|
import Tooltip from '@mui/material/Tooltip'
|
||||||
@@ -59,15 +60,22 @@ export function SocialButton({rezo}) {
|
|||||||
return (
|
return (
|
||||||
<Tooltip title={rezo.url}>
|
<Tooltip title={rezo.url}>
|
||||||
<Chip
|
<Chip
|
||||||
|
clickable
|
||||||
label={config.label}
|
label={config.label}
|
||||||
component='a'
|
component='a'
|
||||||
href={rezo.url}
|
href={rezo.url}
|
||||||
target='_blank'
|
target='_blank'
|
||||||
rel='noopener noreferrer'
|
rel='noopener noreferrer'
|
||||||
clickable
|
|
||||||
size='small'
|
size='small'
|
||||||
sx={{bgcolor: config.bg, color: config.color, fontWeight: 600}}
|
sx={{bgcolor: config.bg, color: config.color, fontWeight: 600}}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</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 => (
|
||||||
|
|||||||
@@ -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 :
|
||||||
|
|||||||
@@ -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 => {
|
||||||
|
|||||||
@@ -48,10 +48,10 @@ export default function KitsDialog({kits}) {
|
|||||||
<DialogTitle>Télécharger un kit</DialogTitle>
|
<DialogTitle>Télécharger un kit</DialogTitle>
|
||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
<List>
|
<List>
|
||||||
{kits.map((kit, index) => {
|
{kits.map(kit => {
|
||||||
const PlatformIcon = PLATFORM_ICON[kit.plateforme] ?? CloudDownloadIcon
|
const PlatformIcon = PLATFORM_ICON[kit.plateforme] ?? CloudDownloadIcon
|
||||||
return (
|
return (
|
||||||
<ListItem key={index} disablePadding>
|
<ListItem key={kit.url} disablePadding>
|
||||||
<ListItemButton component='a' href={kit.url} target='_blank' rel='noopener noreferrer'>
|
<ListItemButton component='a' href={kit.url} target='_blank' rel='noopener noreferrer'>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<PlatformIcon />
|
<PlatformIcon />
|
||||||
|
|||||||
@@ -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,162 +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>
|
|
||||||
<StyledMenuItem
|
|
||||||
id='pt'
|
|
||||||
classes={{
|
|
||||||
root: classes.root
|
|
||||||
}}
|
|
||||||
onClick={handleClose}
|
|
||||||
>🇧🇷 Portugais</StyledMenuItem>
|
|
||||||
<StyledMenuItem
|
|
||||||
id='ja'
|
|
||||||
classes={{
|
|
||||||
root: classes.root
|
|
||||||
}}
|
|
||||||
onClick={handleClose}
|
|
||||||
>🇯🇵 Japonais</StyledMenuItem>
|
|
||||||
<StyledMenuItem
|
|
||||||
id='ko'
|
|
||||||
classes={{
|
|
||||||
root: classes.root
|
|
||||||
}}
|
|
||||||
onClick={handleClose}
|
|
||||||
>🇰🇷 Coréen</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,469 +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: '🇮🇹'
|
|
||||||
},
|
|
||||||
pt: {
|
|
||||||
title: 'Tradução',
|
|
||||||
help: 'Tradução da letra',
|
|
||||||
emoji: '🇧🇷'
|
|
||||||
},
|
|
||||||
ja: {
|
|
||||||
title: '翻訳',
|
|
||||||
help: '歌詞の翻訳',
|
|
||||||
emoji: '🇯🇵'
|
|
||||||
},
|
|
||||||
ko: {
|
|
||||||
title: '번역',
|
|
||||||
help: '가사 번역',
|
|
||||||
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: '', pt: '', ja: '', ko: ''})
|
|
||||||
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, pt, ja, ko} = 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,
|
|
||||||
portugais: pt === '' ? null : pt,
|
|
||||||
japonais: ja === '' ? null : ja,
|
|
||||||
coreen: ko === '' ? null : ko
|
|
||||||
},
|
|
||||||
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,
|
|
||||||
portugais: pt === '' ? null : pt,
|
|
||||||
japonais: ja === '' ? null : ja,
|
|
||||||
coreen: ko === '' ? null : ko
|
|
||||||
},
|
|
||||||
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: '', pt: '', ja: '', ko: ''})
|
|
||||||
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 || '',
|
|
||||||
pt: selectedTeks?.traductions?.portugais || '',
|
|
||||||
ja: selectedTeks?.traductions?.japonais || '',
|
|
||||||
ko: selectedTeks?.traductions?.coreen || ''
|
|
||||||
})
|
|
||||||
|
|
||||||
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')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedTeks?.traductions?.portugais) {
|
|
||||||
ids.push('pt')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedTeks?.traductions?.japonais) {
|
|
||||||
ids.push('ja')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedTeks?.traductions?.coreen) {
|
|
||||||
ids.push('ko')
|
|
||||||
}
|
|
||||||
|
|
||||||
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 !== '' || tradiksyon.pt !== '' || tradiksyon.ja !== '' || tradiksyon.ko !== ''}
|
|
||||||
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,5 +1,6 @@
|
|||||||
'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'
|
||||||
@@ -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,51 @@
|
|||||||
|
import {describe, it, expect} from 'vitest'
|
||||||
|
import {render, screen} from '@testing-library/react'
|
||||||
|
import Komante from '../komante'
|
||||||
|
|
||||||
|
describe('Komante', () => {
|
||||||
|
it('ne rend rien quand il n\'y a aucun commentaire', () => {
|
||||||
|
const {container} = render(<Komante commentaires={[]} />)
|
||||||
|
expect(container).toBeEmptyDOMElement()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('affiche un commentaire local avec le pseudo pawol.nu', () => {
|
||||||
|
render(<Komante commentaires={[
|
||||||
|
{id: 1, contenu: 'Bèl tèks', origine: 'local', user: {username: 'foo'}}
|
||||||
|
]} />)
|
||||||
|
|
||||||
|
expect(screen.getByText('foo')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Bèl tèks')).toBeInTheDocument()
|
||||||
|
expect(screen.queryByText('Mastodon')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('affiche un commentaire fédéré avec le badge et le lien Mastodon', () => {
|
||||||
|
render(<Komante commentaires={[{
|
||||||
|
id: 2,
|
||||||
|
contenu: 'Trè bèl',
|
||||||
|
origine: 'activitypub',
|
||||||
|
auteurNom: 'Quelqu\'un',
|
||||||
|
auteurHandle: '@quelqun@mastodon.social',
|
||||||
|
auteurProfilUrl: 'https://mastodon.social/@quelqun',
|
||||||
|
remoteUrl: 'https://bokante.o-k-i.net/@quelqun/1'
|
||||||
|
}]} />)
|
||||||
|
|
||||||
|
expect(screen.getByText('Quelqu\'un')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Mastodon')).toBeInTheDocument()
|
||||||
|
expect(screen.getByRole('link', {name: 'Quelqu\'un'})).toHaveAttribute('href', 'https://mastodon.social/@quelqun')
|
||||||
|
expect(screen.getByRole('link', {name: /voir sur mastodon/i})).toHaveAttribute('href', 'https://bokante.o-k-i.net/@quelqun/1')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('met en valeur les mentions @ dans le contenu', () => {
|
||||||
|
render(<Komante commentaires={[{
|
||||||
|
id: 3,
|
||||||
|
contenu: '@pawol_nu oh oh ye ye, avec @cybermawonaj@bokante.o-k-i.net aussi',
|
||||||
|
origine: 'activitypub',
|
||||||
|
auteurNom: 'Quelqu\'un'
|
||||||
|
}]} />)
|
||||||
|
|
||||||
|
const mansyon = screen.getByText('@pawol_nu')
|
||||||
|
expect(mansyon).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('@cybermawonaj@bokante.o-k-i.net')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText(/oh oh ye ye/)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import {describe, it, expect, vi, afterEach} from 'vitest'
|
||||||
|
import {render, screen} from '@testing-library/react'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import ReponsMastodon from '../repons-mastodon'
|
||||||
|
|
||||||
|
describe('ReponsMastodon', () => {
|
||||||
|
const originalOpen = window.open
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
window.open = originalOpen
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ne rend rien sans bokanteStatusId', () => {
|
||||||
|
const {container} = render(<ReponsMastodon bokanteStatusId={null} />)
|
||||||
|
expect(container).toBeEmptyDOMElement()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('affiche un lien direct vers le fil bokante', () => {
|
||||||
|
render(<ReponsMastodon bokanteStatusId='112233' />)
|
||||||
|
const lien = screen.getByRole('link', {name: 'Mastodon'})
|
||||||
|
expect(lien).toHaveAttribute('href', 'https://bokante.o-k-i.net/@pawol_nu/112233')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('redirige vers authorize_interaction avec le domaine extrait d\'un identifiant complet', async () => {
|
||||||
|
window.open = vi.fn()
|
||||||
|
const user = userEvent.setup()
|
||||||
|
render(<ReponsMastodon bokanteStatusId='112233' />)
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', {name: /répondre via mastodon/i}))
|
||||||
|
await user.type(screen.getByLabelText(/@vous@votre-instance/i), '@quelqun@mastodon.social')
|
||||||
|
await user.click(screen.getByRole('button', {name: /continuer/i}))
|
||||||
|
|
||||||
|
expect(window.open).toHaveBeenCalledWith(
|
||||||
|
'https://mastodon.social/authorize_interaction?uri=' + encodeURIComponent('https://bokante.o-k-i.net/@pawol_nu/112233'),
|
||||||
|
'_blank',
|
||||||
|
'noopener,noreferrer'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('accepte aussi juste un nom d\'instance sans @', async () => {
|
||||||
|
window.open = vi.fn()
|
||||||
|
const user = userEvent.setup()
|
||||||
|
render(<ReponsMastodon bokanteStatusId='112233' />)
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', {name: /répondre via mastodon/i}))
|
||||||
|
await user.type(screen.getByLabelText(/@vous@votre-instance/i), 'mastodon.social')
|
||||||
|
await user.click(screen.getByRole('button', {name: /continuer/i}))
|
||||||
|
|
||||||
|
expect(window.open).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('https://mastodon.social/authorize_interaction'),
|
||||||
|
'_blank',
|
||||||
|
'noopener,noreferrer'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -8,7 +8,6 @@ export default function AnTeks({parole, paroleId}) {
|
|||||||
<Teks
|
<Teks
|
||||||
parole={parole}
|
parole={parole}
|
||||||
paroleId={paroleId}
|
paroleId={paroleId}
|
||||||
commentaires={parole?.commentaires}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import React, {forwardRef, useState} from 'react'
|
import React, {forwardRef, useState} from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
import {format} from 'date-fns'
|
|
||||||
import {fr} from 'date-fns/locale'
|
|
||||||
|
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Dialog from '@mui/material/Dialog'
|
import Dialog from '@mui/material/Dialog'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
@@ -50,7 +47,7 @@ export default function DiferansDialog({difference}) {
|
|||||||
<List sx={{width: '100%', maxWidth: 360, bgcolor: 'background.paper'}}>
|
<List sx={{width: '100%', maxWidth: 360, bgcolor: 'background.paper'}}>
|
||||||
{difference.map(({id, admin_user, date, jsonDiff}) => {
|
{difference.map(({id, admin_user, date, jsonDiff}) => {
|
||||||
const {firstname} = admin_user
|
const {firstname} = admin_user
|
||||||
const diferansDate = format(new Date(date), 'PPPppp', {locale: fr})
|
const diferansDate = new Intl.DateTimeFormat(undefined, {dateStyle: 'full', timeStyle: 'medium'}).format(new Date(date))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={id}>
|
<React.Fragment key={id}>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import Box from '@mui/material/Box'
|
||||||
|
import Stack from '@mui/material/Stack'
|
||||||
|
import Avatar from '@mui/material/Avatar'
|
||||||
|
import Typography from '@mui/material/Typography'
|
||||||
|
import Chip from '@mui/material/Chip'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import {Mastodon} from '@icons-pack/react-simple-icons'
|
||||||
|
|
||||||
|
function formatAuteur(commentaire) {
|
||||||
|
if (commentaire.origine === 'activitypub') {
|
||||||
|
return commentaire.auteurNom || commentaire.auteurHandle || 'Yon moun'
|
||||||
|
}
|
||||||
|
|
||||||
|
return commentaire.user?.username || 'Anonim'
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDat(commentaire) {
|
||||||
|
if (!commentaire.datePublication) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Intl.DateTimeFormat(undefined, {dateStyle: 'short', timeStyle: 'short'})
|
||||||
|
.format(new Date(commentaire.datePublication))
|
||||||
|
}
|
||||||
|
|
||||||
|
const MENTION_REGEX = /(?:@[\w.-]+){1,2}/g
|
||||||
|
|
||||||
|
function metanValèMansyon(contenu) {
|
||||||
|
const mansyon = contenu.match(MENTION_REGEX) || []
|
||||||
|
return contenu.split(MENTION_REGEX).flatMap((moso, index) => (
|
||||||
|
mansyon[index] ? [
|
||||||
|
moso,
|
||||||
|
<Box key={index} component='span' sx={{color: 'primary.main', fontWeight: 700}}>
|
||||||
|
{mansyon[index]}
|
||||||
|
</Box>
|
||||||
|
] : [moso]
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Komante({commentaires}) {
|
||||||
|
if (!commentaires || commentaires.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{maxWidth: 700, margin: '2em auto 0'}}>
|
||||||
|
<Typography gutterBottom variant='h6'>
|
||||||
|
Komantè ({commentaires.length})
|
||||||
|
</Typography>
|
||||||
|
<Stack spacing={1.5}>
|
||||||
|
{commentaires.map(commentaire => {
|
||||||
|
const auteur = formatAuteur(commentaire)
|
||||||
|
const estFedere = commentaire.origine === 'activitypub'
|
||||||
|
const dat = formatDat(commentaire)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
key={commentaire.id}
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: 1.5,
|
||||||
|
p: 2,
|
||||||
|
borderRadius: 2,
|
||||||
|
bgcolor: 'action.hover'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Avatar src={commentaire.auteurAvatarUrl} alt={auteur} />
|
||||||
|
<Box sx={{flex: 1, minWidth: 0}}>
|
||||||
|
<Stack direction='row' alignItems='center' flexWrap='wrap' rowGap={0.5}>
|
||||||
|
<Stack direction='row' alignItems='center' spacing={1}>
|
||||||
|
<Typography variant='subtitle2' component='div'>
|
||||||
|
{estFedere && commentaire.auteurProfilUrl ? (
|
||||||
|
<Link href={commentaire.auteurProfilUrl} target='_blank' rel='noopener noreferrer'>
|
||||||
|
{auteur}
|
||||||
|
</Link>
|
||||||
|
) : auteur}
|
||||||
|
</Typography>
|
||||||
|
{estFedere && (
|
||||||
|
<Chip
|
||||||
|
label='Mastodon'
|
||||||
|
size='small'
|
||||||
|
icon={<Mastodon size={14} color='#6364FF' title='' />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
{dat && (
|
||||||
|
<Typography variant='caption' color='text.secondary' sx={{ml: 'auto', pl: 2}}>
|
||||||
|
{dat}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
<Typography variant='body2' sx={{mt: 0.5}}>{metanValèMansyon(commentaire.contenu)}</Typography>
|
||||||
|
{estFedere && commentaire.remoteUrl && (
|
||||||
|
<Link href={commentaire.remoteUrl} target='_blank' rel='noopener noreferrer'>
|
||||||
|
<Typography variant='caption' color='text.secondary'>Voir sur Mastodon</Typography>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Komante.propTypes = {
|
||||||
|
commentaires: PropTypes.array
|
||||||
|
}
|
||||||
+20
-25
@@ -30,9 +30,10 @@ function createAudio(src) {
|
|||||||
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()})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,9 +76,12 @@ const TinyText = styled(Typography)({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export default function Lekte({audio, url, parole}) {
|
export default function Lekte({audio, url, parole}) {
|
||||||
const audioRef = useRef(createAudio(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()
|
||||||
@@ -104,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()
|
||||||
@@ -132,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)
|
||||||
@@ -158,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 () => {
|
||||||
@@ -167,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)))
|
||||||
@@ -175,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 => {
|
||||||
@@ -330,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}
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import {useState} from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import Box from '@mui/material/Box'
|
||||||
|
import Button from '@mui/material/Button'
|
||||||
|
import Dialog from '@mui/material/Dialog'
|
||||||
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
|
import DialogContent from '@mui/material/DialogContent'
|
||||||
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
|
import TextField from '@mui/material/TextField'
|
||||||
|
import Typography from '@mui/material/Typography'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
const BOKANTE_URL = process.env.NEXT_PUBLIC_BOKANTE_URL || 'https://bokante.o-k-i.net'
|
||||||
|
const BOKANTE_ACCOUNT = process.env.NEXT_PUBLIC_BOKANTE_ACCOUNT || 'pawol_nu'
|
||||||
|
|
||||||
|
function extraireInstance(saisie) {
|
||||||
|
const valeur = saisie.trim().replace(/^@/, '').replace(/^https?:\/\//, '').replace(/\/$/, '')
|
||||||
|
return valeur.includes('@') ? valeur.split('@').pop() : valeur
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ReponsMastodon({bokanteStatusId}) {
|
||||||
|
const [ouvert, setOuvert] = useState(false)
|
||||||
|
const [saisie, setSaisie] = useState('')
|
||||||
|
|
||||||
|
if (!bokanteStatusId) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const tootUrl = `${BOKANTE_URL}/@${BOKANTE_ACCOUNT}/${bokanteStatusId}`
|
||||||
|
|
||||||
|
const repondre = () => {
|
||||||
|
const instance = extraireInstance(saisie)
|
||||||
|
if (!instance) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
window.open(`https://${instance}/authorize_interaction?uri=${encodeURIComponent(tootUrl)}`, '_blank', 'noopener,noreferrer')
|
||||||
|
setOuvert(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{maxWidth: 700, margin: '1em auto 0', textAlign: 'center'}}>
|
||||||
|
<Typography variant='body2' sx={{mb: 1}}>
|
||||||
|
Rejoignez la discussion sur{' '}
|
||||||
|
<Link href={tootUrl} target='_blank' rel='noopener noreferrer'>Mastodon</Link>
|
||||||
|
</Typography>
|
||||||
|
<Button variant='outlined' size='small' onClick={() => setOuvert(true)}>
|
||||||
|
Répondre via Mastodon
|
||||||
|
</Button>
|
||||||
|
<Dialog open={ouvert} onClose={() => setOuvert(false)}>
|
||||||
|
<DialogTitle>Répondre depuis votre compte Mastodon</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Typography variant='body2' sx={{mb: 2}}>
|
||||||
|
Indiquez votre identifiant (@vous@votre-instance) ou juste le nom de votre
|
||||||
|
instance, vous serez redirigé·e vers votre compte pour répondre.
|
||||||
|
</Typography>
|
||||||
|
<TextField
|
||||||
|
autoFocus
|
||||||
|
fullWidth
|
||||||
|
label='@vous@votre-instance'
|
||||||
|
value={saisie}
|
||||||
|
onChange={event => setSaisie(event.target.value)}
|
||||||
|
onKeyDown={event => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
repondre()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => setOuvert(false)}>Annuler</Button>
|
||||||
|
<Button variant='contained' onClick={repondre}>Continuer</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ReponsMastodon.propTypes = {
|
||||||
|
bokanteStatusId: PropTypes.string
|
||||||
|
}
|
||||||
@@ -3,8 +3,6 @@
|
|||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import {useRouter} from 'next/navigation'
|
import {useRouter} from 'next/navigation'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import {format} from 'date-fns'
|
|
||||||
import {fr} from 'date-fns/locale'
|
|
||||||
import Card from '@mui/material/Card'
|
import Card from '@mui/material/Card'
|
||||||
|
|
||||||
import CardActionArea from '@mui/material/CardActionArea'
|
import CardActionArea from '@mui/material/CardActionArea'
|
||||||
@@ -13,6 +11,7 @@ import Typography from '@mui/material/Typography'
|
|||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Grid from '@mui/material/Grid'
|
import Grid from '@mui/material/Grid'
|
||||||
import ExplicitIcon from '@mui/icons-material/Explicit'
|
import ExplicitIcon from '@mui/icons-material/Explicit'
|
||||||
|
import EventIcon from '@mui/icons-material/Event'
|
||||||
import {styled} from '@mui/material/styles'
|
import {styled} from '@mui/material/styles'
|
||||||
|
|
||||||
import {getAlias} from '../../lib/utils/get-alias'
|
import {getAlias} from '../../lib/utils/get-alias'
|
||||||
@@ -37,7 +36,7 @@ export default function TeksKat({parole}) {
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const {titre, artistes, annee, couverture, createdAt, slug} = parole
|
const {titre, artistes, annee, couverture, createdAt, slug} = parole
|
||||||
|
|
||||||
const datPiblikasyon = format(new Date(createdAt), 'P', {locale: fr})
|
const datPiblikasyon = new Intl.DateTimeFormat(undefined, {dateStyle: 'short'}).format(new Date(createdAt))
|
||||||
const aliases = getAlias(artistes, parole.prioriteArtistes)
|
const aliases = getAlias(artistes, parole.prioriteArtistes)
|
||||||
|
|
||||||
const handleClick = slug => {
|
const handleClick = slug => {
|
||||||
@@ -51,9 +50,9 @@ export default function TeksKat({parole}) {
|
|||||||
<Box sx={{position: 'relative', height: 240}}>
|
<Box sx={{position: 'relative', height: 240}}>
|
||||||
{couverture?.url ? (
|
{couverture?.url ? (
|
||||||
<Image
|
<Image
|
||||||
|
fill
|
||||||
src={`${IMAGE_URL}${formatKuveti(couverture, 'thumbnail')?.url}`}
|
src={`${IMAGE_URL}${formatKuveti(couverture, 'thumbnail')?.url}`}
|
||||||
alt={titre}
|
alt={titre}
|
||||||
fill
|
|
||||||
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'
|
||||||
@@ -103,8 +102,15 @@ export default function TeksKat({parole}) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography align='center' style={{marginTop: '0.5em'}} variant='body1' color='textSecondary' component='p'>
|
<Typography
|
||||||
Publiée le : {datPiblikasyon}
|
align='center'
|
||||||
|
style={{marginTop: '0.5em'}}
|
||||||
|
variant='body1'
|
||||||
|
color='textSecondary'
|
||||||
|
component='p'
|
||||||
|
sx={{display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 0.5}}
|
||||||
|
>
|
||||||
|
<EventIcon fontSize='inherit' /> {datPiblikasyon}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
+114
-43
@@ -3,8 +3,11 @@
|
|||||||
import {useEffect, useState} 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 Tabs from '@mui/material/Tabs'
|
import Select from '@mui/material/Select'
|
||||||
import Tab from '@mui/material/Tab'
|
import MenuItem from '@mui/material/MenuItem'
|
||||||
|
import ListSubheader from '@mui/material/ListSubheader'
|
||||||
|
import Divider from '@mui/material/Divider'
|
||||||
|
import FormControl from '@mui/material/FormControl'
|
||||||
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'
|
||||||
@@ -13,8 +16,9 @@ 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 TranslateIcon from '@mui/icons-material/Translate'
|
||||||
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'
|
import StarIcon from '@mui/icons-material/Star'
|
||||||
|
import ArticleIcon from '@mui/icons-material/Article'
|
||||||
|
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
|
|
||||||
@@ -29,6 +33,8 @@ 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 Komante from './komante'
|
||||||
|
import ReponsMastodon from './repons-mastodon'
|
||||||
import {useCurrentTrack} from './current-track-context'
|
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'
|
||||||
@@ -89,15 +95,34 @@ const LANG_NAMES = {
|
|||||||
pt: 'Português', ja: '日本語', ko: '한국어',
|
pt: 'Português', ja: '日本語', ko: '한국어',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CONTINENTS = ['Afrique', 'Asie', 'Europe']
|
||||||
|
|
||||||
const TRAD_FIELDS = [
|
const TRAD_FIELDS = [
|
||||||
{field: 'anglais', title: 'English'},
|
// Afrique — langues vedettes en premier
|
||||||
{field: 'francais', title: 'Français'},
|
{field: 'yoruba', title: 'Yorùbá', continent: 'Afrique', vedette: true},
|
||||||
{field: 'espagnol', title: 'Español'},
|
{field: 'lingala', title: 'Lingála', continent: 'Afrique', vedette: true},
|
||||||
{field: 'allemand', title: 'Deutsch'},
|
{field: 'wolof', title: 'Wolof', continent: 'Afrique', vedette: true},
|
||||||
{field: 'italien', title: 'Italiano'},
|
{field: 'swahili', title: 'Kiswahili', continent: 'Afrique', vedette: true},
|
||||||
{field: 'portugais', title: 'Português'},
|
{field: 'hausa', title: 'Hausa', continent: 'Afrique', vedette: true},
|
||||||
{field: 'japonais', title: '日本語'},
|
{field: 'arabe', title: 'العربية', continent: 'Afrique'},
|
||||||
{field: 'coreen', title: '한국어'},
|
{field: 'oromo', title: 'Oromoo', continent: 'Afrique'},
|
||||||
|
{field: 'igbo', title: 'Igbo', continent: 'Afrique'},
|
||||||
|
{field: 'zoulou', title: 'isiZulu', continent: 'Afrique'},
|
||||||
|
{field: 'malgache', title: 'Malagasy', continent: 'Afrique'},
|
||||||
|
{field: 'xhosa', title: 'isiXhosa', continent: 'Afrique'},
|
||||||
|
{field: 'tswana', title: 'Setswana', continent: 'Afrique'},
|
||||||
|
{field: 'tsonga', title: 'Xitsonga', continent: 'Afrique'},
|
||||||
|
{field: 'sesotho', title: 'Sesotho', continent: 'Afrique'},
|
||||||
|
// Asie
|
||||||
|
{field: 'japonais', title: '日本語', continent: 'Asie'},
|
||||||
|
{field: 'coreen', title: '한국어', continent: 'Asie'},
|
||||||
|
// Europe
|
||||||
|
{field: 'anglais', title: 'English', continent: 'Europe'},
|
||||||
|
{field: 'francais', title: 'Français', continent: 'Europe'},
|
||||||
|
{field: 'espagnol', title: 'Español', continent: 'Europe'},
|
||||||
|
{field: 'allemand', title: 'Deutsch', continent: 'Europe'},
|
||||||
|
{field: 'italien', title: 'Italiano', continent: 'Europe'},
|
||||||
|
{field: 'portugais', title: 'Português', continent: 'Europe'},
|
||||||
]
|
]
|
||||||
|
|
||||||
const langToArray = parole => {
|
const langToArray = parole => {
|
||||||
@@ -107,7 +132,15 @@ const langToArray = parole => {
|
|||||||
|
|
||||||
return TRAD_FIELDS
|
return TRAD_FIELDS
|
||||||
.filter(({field}) => parole.traductions[field])
|
.filter(({field}) => parole.traductions[field])
|
||||||
.map(({field, title}) => ({title, lang: parole.traductions[field]}))
|
.map(({field, title, continent, vedette}) => ({field, title, continent, vedette, lang: parole.traductions[field]}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const BROWSER_LANG_TO_FIELD = {
|
||||||
|
yo: 'yoruba', ln: 'lingala', wo: 'wolof', sw: 'swahili', ha: 'hausa',
|
||||||
|
ar: 'arabe', om: 'oromo', ig: 'igbo', zu: 'zoulou', mg: 'malgache',
|
||||||
|
xh: 'xhosa', tn: 'tswana', ts: 'tsonga', st: 'sesotho',
|
||||||
|
ja: 'japonais', ko: 'coreen', en: 'anglais', fr: 'francais',
|
||||||
|
es: 'espagnol', de: 'allemand', it: 'italien', pt: 'portugais',
|
||||||
}
|
}
|
||||||
|
|
||||||
const ExplicitTooltip = Tooltip
|
const ExplicitTooltip = Tooltip
|
||||||
@@ -134,6 +167,20 @@ export default function Teks({parole}) {
|
|||||||
return () => setCurrentTrack(null)
|
return () => setCurrentTrack(null)
|
||||||
}, [parole, setCurrentTrack])
|
}, [parole, setCurrentTrack])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const browserLang = navigator.language?.split('-')[0]?.toLowerCase()
|
||||||
|
if (!browserLang || browserLang === parole.langueSource) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const champ = BROWSER_LANG_TO_FIELD[browserLang]
|
||||||
|
const index = langArray.findIndex(lang => lang.field === champ)
|
||||||
|
if (index !== -1) {
|
||||||
|
setTab(index + 1)
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- ne doit s'exécuter qu'au montage, pas à chaque changement d'onglet
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Root className={classes.container}>
|
<Root className={classes.container}>
|
||||||
<Box sx={{textAlign: 'center', marginTop: 12}}>
|
<Box sx={{textAlign: 'center', marginTop: 12}}>
|
||||||
@@ -183,7 +230,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>
|
||||||
@@ -220,47 +267,69 @@ 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>
|
||||||
<Box sx={{maxWidth: 700, margin: '0 auto'}} className={classes.gridText}>
|
<Box sx={{maxWidth: 700, margin: '0 auto'}} className={classes.gridText}>
|
||||||
<Tabs
|
<Box sx={{display: 'flex', justifyContent: 'center', borderBottom: 1, borderColor: 'divider', mb: 2, pb: 2}}>
|
||||||
|
<FormControl size='small' sx={{minWidth: 240, width: isMobile ? '100%' : 'auto'}}>
|
||||||
|
<Select
|
||||||
value={tab}
|
value={tab}
|
||||||
onChange={(event, value) => setTab(value)}
|
MenuProps={{PaperProps: {sx: {maxHeight: 420}}}}
|
||||||
variant='scrollable'
|
renderValue={value => {
|
||||||
scrollButtons
|
const label = value === 0 ? 'Transcription' : langArray[value - 1]?.title
|
||||||
allowScrollButtonsMobile
|
const Icon = value === 0 ? ArticleIcon : TranslateIcon
|
||||||
centered={langArray.length === 0}
|
return (
|
||||||
slots={{
|
<Box sx={{display: 'flex', alignItems: 'center', gap: 1, fontWeight: value === 0 ? 700 : 400}}>
|
||||||
startScrollButtonIcon: ArrowBackIosNewIcon,
|
<Icon fontSize='small' color='primary' />
|
||||||
endScrollButtonIcon: ArrowForwardIosIcon
|
{label}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
|
onChange={event => setTab(event.target.value)}
|
||||||
|
>
|
||||||
|
<MenuItem value={0} sx={{display: 'flex', gap: 0.75, fontWeight: 700}}>
|
||||||
|
<ArticleIcon fontSize='inherit' color='primary' />
|
||||||
|
Transcription
|
||||||
|
</MenuItem>
|
||||||
|
<Divider />
|
||||||
|
{CONTINENTS.flatMap(continent => {
|
||||||
|
const items = langArray
|
||||||
|
.map((item, index) => ({...item, index}))
|
||||||
|
.filter(item => item.continent === continent)
|
||||||
|
|
||||||
|
if (items.length === 0) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
<ListSubheader
|
||||||
|
key={`entet-${continent}`}
|
||||||
sx={{
|
sx={{
|
||||||
borderBottom: 1,
|
fontWeight: 700,
|
||||||
borderColor: 'divider',
|
textTransform: 'uppercase',
|
||||||
mb: 2,
|
letterSpacing: 1,
|
||||||
'& .MuiTabs-scrollButtons': {
|
|
||||||
width: 36,
|
|
||||||
height: 36,
|
|
||||||
borderRadius: '50%',
|
|
||||||
color: 'primary.main',
|
color: 'primary.main',
|
||||||
backgroundColor: 'action.hover',
|
lineHeight: '2.5em'
|
||||||
mx: 0.5,
|
|
||||||
'&.Mui-disabled': {
|
|
||||||
opacity: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tab label='Transcription' />
|
{continent}
|
||||||
{langArray.map(({title}) => (
|
</ListSubheader>,
|
||||||
<Tab key={title} label={title} />
|
...items.map(({title, index, vedette}) => (
|
||||||
))}
|
<MenuItem key={title} value={index + 1} sx={{display: 'flex', gap: 0.75}}>
|
||||||
</Tabs>
|
{vedette && <StarIcon fontSize='inherit' color='primary' />}
|
||||||
|
{title}
|
||||||
|
</MenuItem>
|
||||||
|
))
|
||||||
|
]
|
||||||
|
})}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
{tab === 0 && (
|
{tab === 0 && (
|
||||||
<>
|
<>
|
||||||
{parole.langueSource && parole.langueSource !== 'ka' && (
|
{parole.langueSource && parole.langueSource !== 'ka' && (
|
||||||
@@ -281,6 +350,8 @@ export default function Teks({parole}) {
|
|||||||
)
|
)
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
|
<ReponsMastodon bokanteStatusId={parole.bokanteStatusId} />
|
||||||
|
<Komante commentaires={parole.commentaires} />
|
||||||
</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
|
||||||
|
|||||||
+8
-2
@@ -6,10 +6,16 @@ const SIZE_ORDER = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-4
@@ -2,7 +2,7 @@ import qs from 'qs'
|
|||||||
|
|
||||||
const OKI_API = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:1337'
|
const OKI_API = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:1337'
|
||||||
const AWTIS_POU_CHAK_PAJ = process.env.NEXT_PUBLIC_AWTIS_POU_CHAK_PAJ || 6
|
const AWTIS_POU_CHAK_PAJ = process.env.NEXT_PUBLIC_AWTIS_POU_CHAK_PAJ || 6
|
||||||
const readToken = process.env.NEXT_PUBLIC_READ_TOKEN || 'read-token'
|
const readToken = process.env.API_READ_TOKEN || 'read-token'
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
next: {revalidate: 60},
|
next: {revalidate: 60},
|
||||||
@@ -38,7 +38,16 @@ export async function jwennTeksEpiSlug(slug) {
|
|||||||
fields: ['username', 'firstname']
|
fields: ['username', 'firstname']
|
||||||
},
|
},
|
||||||
commentaires: {
|
commentaires: {
|
||||||
fields: ['contenu', 'datePublication'],
|
fields: [
|
||||||
|
'contenu',
|
||||||
|
'datePublication',
|
||||||
|
'origine',
|
||||||
|
'auteurNom',
|
||||||
|
'auteurHandle',
|
||||||
|
'auteurAvatarUrl',
|
||||||
|
'auteurProfilUrl',
|
||||||
|
'remoteUrl'
|
||||||
|
],
|
||||||
populate: {
|
populate: {
|
||||||
user: {
|
user: {
|
||||||
fields: ['username']
|
fields: ['username']
|
||||||
@@ -186,6 +195,7 @@ async function jwennTouSlug(endpoint) {
|
|||||||
encodeValuesOnly: true
|
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)
|
const {data} = await request(`/${endpoint}?${query}`, headers)
|
||||||
slugs.push(...data.map(({slug}) => slug))
|
slugs.push(...data.map(({slug}) => slug))
|
||||||
hasMore = data.length === pageSize
|
hasMore = data.length === pageSize
|
||||||
@@ -223,9 +233,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'
|
||||||
|
|||||||
+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()
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-13
@@ -34,37 +34,31 @@
|
|||||||
"@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/dom": "^10.4.1",
|
||||||
"@testing-library/jest-dom": "^6.9.1",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@testing-library/react": "^16.3.2",
|
"@testing-library/react": "^16.3.2",
|
||||||
"@testing-library/user-event": "^14.6.1",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
"@vitejs/plugin-react": "^6.0.3",
|
"@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",
|
||||||
@@ -91,11 +85,11 @@
|
|||||||
"error",
|
"error",
|
||||||
{
|
{
|
||||||
"capIsNewExceptions": [
|
"capIsNewExceptions": [
|
||||||
"CredentialsProvider",
|
"GET",
|
||||||
"TwitterProvider",
|
"POST",
|
||||||
"GoogleProvider",
|
"PUT",
|
||||||
"GitHubProvider",
|
"DELETE",
|
||||||
"NextAuth"
|
"PATCH"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
import '@testing-library/jest-dom/vitest'
|
import '@testing-library/jest-dom/vitest' // eslint-disable-line import/no-unassigned-import
|
||||||
|
|||||||
Reference in New Issue
Block a user