new base for gitea

This commit is contained in:
EvanChal
2025-12-23 19:12:30 +01:00
parent 849167994c
commit e916dd7cd0
147 changed files with 757 additions and 15674 deletions

15
frontend/Dockerfile Normal file
View File

@@ -0,0 +1,15 @@
FROM node:18-alpine
WORKDIR /app
# Copy package files first for better caching
COPY package*.json ./
# Install dependencies
RUN npm ci
# Copy application files
COPY . .
# Run the application in development mode
CMD ["npm", "run", "dev"]

View File

@@ -1,30 +0,0 @@
FROM node:18-alpine
# Métadonnées
LABEL maintainer="LeDiscord Team"
LABEL version="1.0"
LABEL description="LeDiscord Frontend - Environnement Development"
WORKDIR /app
# Variables d'environnement pour le développement
ENV NODE_ENV=development
ENV VITE_ENVIRONMENT=development
# Copy package files first for better caching
COPY package*.json ./
# Install dependencies
RUN npm ci
# Copy application files
COPY . .
# Copy development environment file
COPY .env.development .env
# Expose port
EXPOSE 5173
# Run the application in development mode
CMD ["npm", "run", "dev"]

View File

@@ -1,50 +0,0 @@
# Multi-stage build pour la production
FROM node:18-alpine AS builder
# Métadonnées
LABEL maintainer="LeDiscord Team"
LABEL version="1.0"
LABEL description="LeDiscord Frontend - Build Production"
WORKDIR /app
# Variables d'environnement pour la production
ENV NODE_ENV=production
ENV VITE_ENVIRONMENT=production
# Copy package files first for better caching
COPY package*.json ./
# Install dependencies
RUN npm ci
# Copy application files
COPY . .
# Copy production environment file
COPY .env.production .env
# Debug: Vérifier le contenu du fichier .env
RUN echo "=== Contenu du fichier .env ===" && cat .env
RUN echo "=== Variables d'environnement ===" && env | grep VITE
# Charger les variables d'environnement depuis le fichier .env (en filtrant les commentaires)
RUN export $(cat .env | grep -v '^#' | grep -v '^$' | xargs) && echo "=== Variables après export ===" && env | grep VITE
# Build the application avec les variables d'environnement chargées
RUN export $(cat .env | grep -v '^#' | grep -v '^$' | xargs) && npm run build
# Production stage
FROM nginx:alpine
# Copy built application
COPY --from=builder /app/dist /usr/share/nginx/html
# Copy nginx configuration
COPY nginx-frontend.conf /etc/nginx/nginx.conf
# Expose port
EXPOSE 80
# Start nginx
CMD ["nginx", "-g", "daemon off;"]

0
frontend/index.html Executable file → Normal file
View File

View File

@@ -1,64 +0,0 @@
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Logging
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log warn;
# Basic settings
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied expired no-cache no-store private auth;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss;
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Static files with long cache
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
# Vue Router - SPA fallback
location / {
try_files $uri $uri/ /index.html;
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
# Health check
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}
}

0
frontend/package-lock.json generated Executable file → Normal file
View File

0
frontend/package.json Executable file → Normal file
View File

0
frontend/postcss.config.js Executable file → Normal file
View File

0
frontend/src/App.vue Executable file → Normal file
View File

0
frontend/src/components/EnvironmentDebug.vue Executable file → Normal file
View File

View File

@@ -0,0 +1,167 @@
<template>
<div class="form-validation">
<div v-if="errors.length > 0" class="mt-2 space-y-1">
<div
v-for="error in errors"
:key="error"
class="flex items-center text-sm text-red-600"
>
<svg class="w-4 h-4 mr-1 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
{{ error }}
</div>
</div>
<div v-if="warnings.length > 0" class="mt-2 space-y-1">
<div
v-for="warning in warnings"
:key="warning"
class="flex items-center text-sm text-yellow-600"
>
<svg class="w-4 h-4 mr-1 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
{{ warning }}
</div>
</div>
<div v-if="successes.length > 0" class="mt-2 space-y-1">
<div
v-for="success in successes"
:key="success"
class="flex items-center text-sm text-green-600"
>
<svg class="w-4 h-4 mr-1 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg>
{{ success }}
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
field: {
type: String,
required: true
},
value: {
type: String,
required: true
},
touched: {
type: Boolean,
default: false
}
})
const errors = computed(() => {
if (!props.touched) return []
const fieldErrors = []
switch (props.field) {
case 'email':
if (!props.value) {
fieldErrors.push('L\'email est requis')
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(props.value)) {
fieldErrors.push('Format d\'email invalide')
}
break
case 'username':
if (!props.value) {
fieldErrors.push('Le nom d\'utilisateur est requis')
} else if (props.value.length < 3) {
fieldErrors.push('Le nom d\'utilisateur doit contenir au moins 3 caractères')
} else if (props.value.length > 50) {
fieldErrors.push('Le nom d\'utilisateur ne peut pas dépasser 50 caractères')
} else if (!/^[a-zA-Z0-9_-]+$/.test(props.value)) {
fieldErrors.push('Le nom d\'utilisateur ne peut contenir que des lettres, chiffres, tirets et underscores')
}
break
case 'full_name':
if (!props.value) {
fieldErrors.push('Le nom complet est requis')
} else if (props.value.length < 2) {
fieldErrors.push('Le nom complet doit contenir au moins 2 caractères')
}
break
case 'password':
if (!props.value) {
fieldErrors.push('Le mot de passe est requis')
} else if (props.value.length < 6) {
fieldErrors.push('Le mot de passe doit contenir au moins 6 caractères')
}
break
case 'password_confirm':
// Cette validation sera gérée au niveau du composant parent
break
}
return fieldErrors
})
const warnings = computed(() => {
if (!props.touched) return []
const fieldWarnings = []
switch (props.field) {
case 'username':
if (props.value && props.value.length < 5) {
fieldWarnings.push('Un nom d\'utilisateur plus long sera plus facile à retenir')
}
break
case 'password':
if (props.value && props.value.length < 8) {
fieldWarnings.push('Un mot de passe de 8 caractères ou plus est recommandé')
}
break
}
return fieldWarnings
})
const successes = computed(() => {
if (!props.touched) return []
const fieldSuccesses = []
switch (props.field) {
case 'email':
if (props.value && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(props.value)) {
fieldSuccesses.push('Format d\'email valide')
}
break
case 'username':
if (props.value && props.value.length >= 3 && /^[a-zA-Z0-9_-]+$/.test(props.value)) {
fieldSuccesses.push('Nom d\'utilisateur valide')
}
break
case 'full_name':
if (props.value && props.value.length >= 2) {
fieldSuccesses.push('Nom complet valide')
}
break
case 'password':
if (props.value && props.value.length >= 6) {
fieldSuccesses.push('Longueur du mot de passe correcte')
}
break
}
return fieldSuccesses
})
</script>

0
frontend/src/components/MentionInput.vue Executable file → Normal file
View File

0
frontend/src/components/Mentions.vue Executable file → Normal file
View File

View File

@@ -0,0 +1,110 @@
<template>
<div class="password-strength">
<div class="flex items-center justify-between mb-2">
<span class="text-sm text-gray-600">Force du mot de passe</span>
<span class="text-sm font-medium" :class="strengthColor">{{ strengthText }}</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2 mb-2">
<div
class="h-2 rounded-full transition-all duration-300"
:class="strengthBarColor"
:style="{ width: `${strengthPercentage}%` }"
></div>
</div>
<div class="text-xs text-gray-500 space-y-1">
<div class="flex items-center" :class="{ 'text-green-600': hasMinLength }">
<svg v-if="hasMinLength" class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
<svg v-else class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg>
Au moins 6 caractères
</div>
<div class="flex items-center" :class="{ 'text-green-600': hasUppercase }">
<svg v-if="hasUppercase" class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
<svg v-else class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg>
Au moins une majuscule
</div>
<div class="flex items-center" :class="{ 'text-green-600': hasLowercase }">
<svg v-if="hasLowercase" class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
<svg v-else class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg>
Au moins une minuscule
</div>
<div class="flex items-center" :class="{ 'text-green-600': hasNumber }">
<svg v-if="hasNumber" class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
<svg v-else class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg>
Au moins un chiffre
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
password: {
type: String,
required: true
}
})
const hasMinLength = computed(() => props.password.length >= 6)
const hasUppercase = computed(() => /[A-Z]/.test(props.password))
const hasLowercase = computed(() => /[a-z]/.test(props.password))
const hasNumber = computed(() => /\d/.test(props.password))
const strengthScore = computed(() => {
let score = 0
if (hasMinLength.value) score += 1
if (hasUppercase.value) score += 1
if (hasLowercase.value) score += 1
if (hasNumber.value) score += 1
if (props.password.length >= 8) score += 1
return score
})
const strengthPercentage = computed(() => (strengthScore.value / 5) * 100)
const strengthText = computed(() => {
if (strengthScore.value <= 1) return 'Très faible'
if (strengthScore.value <= 2) return 'Faible'
if (strengthScore.value <= 3) return 'Moyen'
if (strengthScore.value <= 4) return 'Fort'
return 'Très fort'
})
const strengthColor = computed(() => {
if (strengthScore.value <= 1) return 'text-red-600'
if (strengthScore.value <= 2) return 'text-orange-600'
if (strengthScore.value <= 3) return 'text-yellow-600'
if (strengthScore.value <= 4) return 'text-blue-600'
return 'text-green-600'
})
const strengthBarColor = computed(() => {
if (strengthScore.value <= 1) return 'bg-red-500'
if (strengthScore.value <= 2) return 'bg-orange-500'
if (strengthScore.value <= 3) return 'bg-yellow-500'
if (strengthScore.value <= 4) return 'bg-blue-500'
return 'bg-green-500'
})
</script>

View File

@@ -0,0 +1,130 @@
<template>
<div class="step-navigation mb-8">
<!-- Progress Bar -->
<div class="mb-6">
<div class="flex items-center justify-between mb-2">
<span class="text-sm text-gray-600">Étape {{ currentStep }} sur {{ totalSteps }}</span>
<span class="text-sm text-gray-500">{{ Math.round((currentStep / totalSteps) * 100) }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-1">
<div
class="bg-primary-600 h-1 rounded-full transition-all duration-300"
:style="{ width: `${(currentStep / totalSteps) * 100}%` }"
></div>
</div>
</div>
<!-- Step Indicators -->
<div class="flex justify-between items-center mb-8">
<div
v-for="step in totalSteps"
:key="step"
class="flex flex-col items-center"
>
<div
class="w-8 h-8 rounded-full flex items-center justify-center text-xs font-medium transition-colors"
:class="getStepClasses(step)"
>
<span v-if="step < currentStep" class="text-white">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
</span>
<span v-else>{{ step }}</span>
</div>
<span
class="text-xs mt-2 text-center max-w-16"
:class="getStepTextClasses(step)"
>
{{ getStepTitle(step) }}
</span>
</div>
</div>
<!-- Navigation Buttons -->
<div class="flex justify-between">
<button
v-if="currentStep > 1"
@click="$emit('previous')"
class="btn-secondary px-4 py-2 text-sm"
:disabled="loading"
>
Précédent
</button>
<div v-else></div>
<button
v-if="currentStep < totalSteps"
@click="$emit('next')"
:disabled="!canProceed || loading"
class="btn-primary px-4 py-2 text-sm"
:class="{ 'opacity-50 cursor-not-allowed': !canProceed || loading }"
>
Suivant
</button>
<button
v-if="currentStep === totalSteps"
@click="$emit('submit')"
:disabled="loading"
class="btn-primary px-4 py-2 text-sm"
>
{{ loading ? 'Création...' : 'Créer mon compte' }}
</button>
</div>
</div>
</template>
<script setup>
const props = defineProps({
currentStep: {
type: Number,
required: true
},
totalSteps: {
type: Number,
required: true
},
canProceed: {
type: Boolean,
default: true
},
loading: {
type: Boolean,
default: false
}
})
defineEmits(['previous', 'next', 'submit'])
function getStepClasses(step) {
if (step < props.currentStep) {
return 'bg-green-500 text-white'
} else if (step === props.currentStep) {
return 'bg-blue-600 text-white ring-4 ring-blue-200'
} else {
return 'bg-gray-200 text-gray-500'
}
}
function getStepTextClasses(step) {
if (step < props.currentStep) {
return 'text-green-600 font-medium'
} else if (step === props.currentStep) {
return 'text-blue-600 font-medium'
} else {
return 'text-gray-500'
}
}
function getStepTitle(step) {
const titles = {
1: 'Bienvenue',
2: 'Informations',
3: 'Avertissement',
4: 'Fonctionnalités',
5: 'Confirmation'
}
return titles[step] || `Étape ${step}`
}
</script>

View File

@@ -0,0 +1,47 @@
<template>
<transition
name="step"
mode="out-in"
appear
>
<div :key="step" class="step-content">
<slot />
</div>
</transition>
</template>
<script setup>
defineProps({
step: {
type: Number,
required: true
}
})
</script>
<style scoped>
.step-content {
min-height: 400px;
}
.step-enter-active,
.step-leave-active {
transition: all 0.5s ease;
}
.step-enter-from {
opacity: 0;
transform: translateX(30px);
}
.step-leave-to {
opacity: 0;
transform: translateX(-30px);
}
.step-enter-to,
.step-leave-from {
opacity: 1;
transform: translateX(0);
}
</style>

0
frontend/src/components/TicketFloatingButton.vue Executable file → Normal file
View File

0
frontend/src/components/UserAvatar.vue Executable file → Normal file
View File

0
frontend/src/components/VideoPlayer.vue Executable file → Normal file
View File

0
frontend/src/components/VlogComments.vue Executable file → Normal file
View File

0
frontend/src/layouts/AuthLayout.vue Executable file → Normal file
View File

0
frontend/src/layouts/DefaultLayout.vue Executable file → Normal file
View File

0
frontend/src/main.js Executable file → Normal file
View File

0
frontend/src/router/index.js Executable file → Normal file
View File

0
frontend/src/stores/auth.js Executable file → Normal file
View File

0
frontend/src/style.css Executable file → Normal file
View File

0
frontend/src/utils/axios.js Executable file → Normal file
View File

0
frontend/src/views/Admin.vue Executable file → Normal file
View File

0
frontend/src/views/AlbumDetail.vue Executable file → Normal file
View File

0
frontend/src/views/Albums.vue Executable file → Normal file
View File

0
frontend/src/views/EventDetail.vue Executable file → Normal file
View File

0
frontend/src/views/Events.vue Executable file → Normal file
View File

0
frontend/src/views/Home.vue Executable file → Normal file
View File

0
frontend/src/views/Information.vue Executable file → Normal file
View File

0
frontend/src/views/Login.vue Executable file → Normal file
View File

0
frontend/src/views/MyTickets.vue Executable file → Normal file
View File

0
frontend/src/views/Posts.vue Executable file → Normal file
View File

0
frontend/src/views/Profile.vue Executable file → Normal file
View File

341
frontend/src/views/Register.vue Executable file → Normal file
View File

@@ -1,96 +1,141 @@
<template>
<div>
<h2 class="text-2xl font-bold text-gray-900 mb-6">Inscription</h2>
<form @submit.prevent="handleRegister" class="space-y-4">
<div>
<label for="email" class="label">Email</label>
<input
id="email"
v-model="form.email"
type="email"
required
:disabled="!registrationEnabled"
class="input"
:class="{ 'opacity-50 cursor-not-allowed': !registrationEnabled }"
placeholder="ton.email@example.com"
>
<div class="space-y-6">
<!-- Header -->
<div class="text-center">
<h2 class="text-2xl font-bold text-gray-900">Créer un compte</h2>
<p class="mt-2 text-sm text-gray-500">Rejoignez notre communauté en quelques étapes</p>
</div>
<!-- Progress Bar -->
<div class="pt-4">
<div class="flex items-center justify-between mb-1">
<span class="text-sm font-medium text-gray-700">Étape {{ currentStep }} sur {{ totalSteps }}</span>
<span class="text-sm font-medium text-gray-500">{{ Math.round((currentStep / totalSteps) * 100) }}%</span>
</div>
<div>
<label for="username" class="label">Nom d'utilisateur</label>
<input
id="username"
v-model="form.username"
type="text"
required
minlength="3"
maxlength="50"
:disabled="!registrationEnabled"
class="input"
:class="{ 'opacity-50 cursor-not-allowed': !registrationEnabled }"
placeholder="tonpseudo"
>
<div class="w-full bg-gray-200 rounded-full h-1.5">
<div
class="bg-primary-600 h-1.5 rounded-full transition-all duration-500 ease-in-out"
:style="{ width: `${(currentStep / totalSteps) * 100}%` }"
></div>
</div>
<div>
<label for="full_name" class="label">Nom complet</label>
<input
id="full_name"
v-model="form.full_name"
type="text"
required
:disabled="!registrationEnabled"
class="input"
:class="{ 'opacity-50 cursor-not-allowed': !registrationEnabled }"
placeholder="Prénom Nom"
>
</div>
<div>
<label for="password" class="label">Mot de passe</label>
<input
id="password"
v-model="form.password"
type="password"
required
minlength="6"
:disabled="!registrationEnabled"
class="input"
:class="{ 'opacity-50 cursor-not-allowed': !registrationEnabled }"
placeholder="••••••••"
>
</div>
<div>
<label for="password_confirm" class="label">Confirmer le mot de passe</label>
<input
id="password_confirm"
v-model="form.password_confirm"
type="password"
required
minlength="6"
:disabled="!registrationEnabled"
class="input"
:class="{ 'opacity-50 cursor-not-allowed': !registrationEnabled }"
placeholder="••••••••"
>
</div>
<div v-if="error" class="text-red-600 text-sm">
{{ error }}
</div>
</div>
<!-- Step Content -->
<div class="min-h-[350px] flex flex-col justify-center">
<StepTransition :step="currentStep">
<!-- Step 1: Welcome -->
<div v-if="currentStep === 1" class="text-center">
<h3 class="text-2xl font-bold text-gray-900 mb-4">Bienvenue sur LeDiscord !</h3>
<p class="text-gray-600 max-w-sm mx-auto">
Nous sommes ravis de vous accueillir. Préparez-vous à rejoindre une communauté passionnante.
</p>
</div>
<!-- Step 2: Registration Form -->
<div v-if="currentStep === 2">
<form @submit.prevent="nextStep" class="space-y-6">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-4">
<div>
<label for="email" class="label">Email</label>
<input id="email" v-model="form.email" type="email" required class="input" @blur="touchedFields.email = true">
</div>
<div>
<label for="username" class="label">Nom d'utilisateur</label>
<input id="username" v-model="form.username" type="text" required minlength="3" class="input" @blur="touchedFields.username = true">
</div>
</div>
<div>
<label for="full_name" class="label">Nom complet</label>
<input id="full_name" v-model="form.full_name" type="text" required class="input" @blur="touchedFields.full_name = true">
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-4">
<div>
<label for="password" class="label">Mot de passe</label>
<input id="password" v-model="form.password" type="password" required minlength="6" class="input" @blur="touchedFields.password = true">
</div>
<div>
<label for="password_confirm" class="label">Confirmer</label>
<input id="password_confirm" v-model="form.password_confirm" type="password" required class="input" @blur="touchedFields.password_confirm = true">
</div>
</div>
<PasswordStrength :password="form.password" />
<div v-if="touchedFields.password_confirm && form.password_confirm && form.password !== form.password_confirm" class="flex items-center text-sm text-red-600">
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd" /></svg>
Les mots de passe ne correspondent pas
</div>
</form>
</div>
<!-- Step 3: Warning -->
<div v-if="currentStep === 3" class="text-center">
<div class="bg-yellow-50 border-l-4 border-yellow-400 p-4">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3 text-left">
<p class="text-sm text-yellow-700">
LeDiscord est actuellement en version bêta. Votre retour est précieux pour nous aider à améliorer la plateforme.
</p>
</div>
</div>
</div>
</div>
<!-- Step 4: Features Tour -->
<div v-if="currentStep === 4" class="text-center">
<h3 class="text-2xl font-bold text-gray-900 mb-4">Découvrez les fonctionnalités</h3>
<div class="space-y-4">
<div v-for="feature in features" :key="feature.title" class="border rounded-lg p-4 text-left flex items-center space-x-4">
<div class="w-10 h-10 bg-gray-100 rounded-lg flex items-center justify-center">
<span v-html="feature.icon"></span>
</div>
<div>
<h4 class="font-semibold">{{ feature.title }}</h4>
<p class="text-sm text-gray-600">{{ feature.description }}</p>
</div>
</div>
</div>
</div>
</StepTransition>
</div>
<!-- Navigation Buttons -->
<div class="flex items-center pt-6" :class="currentStep > 1 ? 'justify-between' : 'justify-end'">
<button
type="submit"
:disabled="loading || !registrationEnabled"
class="w-full btn-primary"
v-if="currentStep > 1"
@click="previousStep"
class="btn-secondary"
:disabled="loading"
>Précédent</button>
<button
v-if="currentStep < totalSteps"
@click="nextStep"
:disabled="!canProceed || loading"
class="btn-primary bg-gradient-to-r from-primary-500 to-purple-600"
:class="{ 'opacity-50 cursor-not-allowed': !canProceed }"
>Suivant</button>
<button
v-if="currentStep === totalSteps"
@click="handleRegister"
:disabled="loading"
class="btn-primary bg-gradient-to-r from-primary-500 to-purple-600"
>
{{ loading ? 'Inscription...' : !registrationEnabled ? 'Inscriptions désactivées' : 'S\'inscrire' }}
<span v-if="loading">Création en cours...</span>
<span v-else>Créer mon compte</span>
</button>
</form>
</div>
<div class="mt-6 text-center">
<div v-if="error" class="mt-4 text-center text-red-600">
{{ error }}
</div>
<!-- Login Link -->
<div class="mt-8 text-center">
<p class="text-sm text-gray-600">
Déjà un compte ?
<router-link to="/login" class="font-medium text-primary-600 hover:text-primary-500">
@@ -98,33 +143,25 @@
</router-link>
</p>
</div>
<!-- Vérification du statut d'inscription -->
<div v-if="!registrationEnabled" class="mt-6 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-yellow-800">Inscriptions désactivées</h3>
<p class="text-sm text-yellow-700 mt-1">
Les nouvelles inscriptions sont temporairement désactivées. Veuillez contacter l'administrateur.
</p>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import axios from '@/utils/axios'
import StepTransition from '@/components/StepTransition.vue'
import PasswordStrength from '@/components/PasswordStrength.vue'
import FormValidation from '@/components/FormValidation.vue'
const authStore = useAuthStore()
const router = useRouter()
// Step management
const currentStep = ref(1)
const totalSteps = 4
// Form data
const form = ref({
email: '',
username: '',
@@ -133,23 +170,63 @@ const form = ref({
password_confirm: ''
})
const touchedFields = ref({
email: false,
username: false,
full_name: false,
password: false,
password_confirm: false
})
const loading = ref(false)
const error = ref('')
const registrationEnabled = ref(true)
// Features for the tour
const features = ref([
{
title: 'Posts & Discussions',
description: 'Partagez vos idées et participez.',
icon: '<svg class="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"></path></svg>'
},
{
title: 'Vlogs & Vidéos',
description: 'Regardez des vidéos de la communauté.',
icon: '<svg class="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>'
},
{
title: 'Événements',
description: 'Participez à des rencontres.',
icon: '<svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>'
}
])
// Computed properties
const canProceed = computed(() => {
if (currentStep.value === 2) {
return form.value.email && form.value.username && form.value.full_name &&
form.value.password && form.value.password.length >= 6 &&
form.value.password === form.value.password_confirm
}
return true
})
// Methods
function nextStep() {
if (currentStep.value < totalSteps && canProceed.value) {
currentStep.value++
}
}
function previousStep() {
if (currentStep.value > 1) {
currentStep.value--
}
}
async function handleRegister() {
error.value = ''
if (!registrationEnabled.value) {
error.value = 'Les inscriptions sont actuellement désactivées'
return
}
if (form.value.password !== form.value.password_confirm) {
error.value = 'Les mots de passe ne correspondent pas'
return
}
if (!canProceed.value) return
loading.value = true
const result = await authStore.register({
email: form.value.email,
@@ -158,33 +235,11 @@ async function handleRegister() {
password: form.value.password
})
if (!result.success) {
error.value = result.error
if (result.success) {
router.push('/')
} else {
error.value = result.error || 'Une erreur est survenue lors de l\'inscription.'
}
loading.value = false
}
async function checkRegistrationStatus() {
try {
const response = await axios.get('/api/settings/public/registration-status')
const status = response.data
registrationEnabled.value = status.can_register
// Afficher des informations supplémentaires si l'inscription est désactivée
if (!status.registration_enabled) {
console.log('Registration disabled by admin')
} else if (status.current_users_count >= status.max_users) {
console.log(`Maximum users reached: ${status.current_users_count}/${status.max_users}`)
}
} catch (error) {
console.error('Error checking registration status:', error)
// En cas d'erreur, on désactive l'inscription pour éviter les problèmes de sécurité
registrationEnabled.value = false
}
}
onMounted(() => {
checkRegistrationStatus()
})
</script>

0
frontend/src/views/Stats.vue Executable file → Normal file
View File

0
frontend/src/views/UserProfile.vue Executable file → Normal file
View File

0
frontend/src/views/VlogDetail.vue Executable file → Normal file
View File

0
frontend/src/views/Vlogs.vue Executable file → Normal file
View File

0
frontend/tailwind.config.js Executable file → Normal file
View File

View File

@@ -1,222 +0,0 @@
#!/usr/bin/env node
/**
* Script de test des environnements Frontend LeDiscord
* Vérifie la cohérence des configurations et évite les problèmes de mixed content
*/
const fs = require('fs')
const path = require('path')
// Couleurs pour la console
const colors = {
reset: '\x1b[0m',
bright: '\x1b[1m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m'
}
function log(message, color = 'reset') {
console.log(`${colors[color]}${message}${colors.reset}`)
}
function testEnvironment(envName) {
log(`\n🔍 Test de l'environnement: ${envName.toUpperCase()}`, 'cyan')
const envFile = path.join(__dirname, `env.${envName}`)
if (!fs.existsSync(envFile)) {
log(`❌ Fichier ${envFile} non trouvé`, 'red')
return false
}
log(`✅ Fichier ${envFile} trouvé`, 'green')
// Lire le fichier d'environnement
const envContent = fs.readFileSync(envFile, 'utf8')
const envVars = {}
// Parser les variables d'environnement
envContent.split('\n').forEach(line => {
line = line.trim()
if (line && !line.startsWith('#')) {
const [key, ...valueParts] = line.split('=')
if (key && valueParts.length > 0) {
envVars[key] = valueParts.join('=')
}
}
})
// Vérifier les variables requises
const requiredVars = ['VITE_API_URL', 'VITE_APP_URL', 'VITE_UPLOAD_URL', 'VITE_ENVIRONMENT']
let allValid = true
requiredVars.forEach(varName => {
if (!envVars[varName]) {
log(`❌ Variable manquante: ${varName}`, 'red')
allValid = false
} else {
log(`${varName}: ${envVars[varName]}`, 'green')
}
})
// Vérifier la cohérence des protocoles
if (envVars.VITE_API_URL && envVars.VITE_APP_URL) {
const apiProtocol = envVars.VITE_API_URL.split('://')[0]
const appProtocol = envVars.VITE_APP_URL.split('://')[0]
if (apiProtocol !== appProtocol) {
log(`⚠️ Protocoles différents: API=${apiProtocol}, APP=${appProtocol}`, 'yellow')
log(` Cela peut causer des problèmes de mixed content!`, 'yellow')
allValid = false
} else {
log(`✅ Protocoles cohérents: ${apiProtocol}`, 'green')
}
}
// Vérifier la cohérence des domaines
if (envVars.VITE_API_URL && envVars.VITE_APP_URL) {
const apiDomain = envVars.VITE_API_URL.split('://')[1]?.split('/')[0]
const appDomain = envVars.VITE_APP_URL.split('://')[1]?.split('/')[0]
if (apiDomain && appDomain) {
if (envName === 'local') {
// En local, les domaines peuvent être différents (localhost:8000 vs localhost:5173)
log(`✅ Domaines locaux: API=${apiDomain}, APP=${appDomain}`, 'green')
} else if (apiDomain.includes('dev.') && appDomain.includes('dev.')) {
log(`✅ Domaines de développement cohérents: ${apiDomain}`, 'green')
} else if (!apiDomain.includes('dev.') && !appDomain.includes('dev.')) {
log(`✅ Domaines de production cohérents: ${apiDomain}`, 'green')
} else {
log(`⚠️ Domaines incohérents: API=${apiDomain}, APP=${appDomain}`, 'yellow')
allValid = false
}
}
}
// Vérifier l'environnement spécifié
if (envVars.VITE_ENVIRONMENT && envVars.VITE_ENVIRONMENT !== envName) {
log(`⚠️ Environnement spécifié (${envVars.VITE_ENVIRONMENT}) ne correspond pas au nom du fichier (${envName})`, 'yellow')
allValid = false
}
return allValid
}
function testDockerfiles() {
log(`\n🐳 Test des Dockerfiles`, 'cyan')
const dockerfiles = ['Dockerfile.local', 'Dockerfile.dev', 'Dockerfile.prod']
let allValid = true
dockerfiles.forEach(dockerfile => {
const dockerfilePath = path.join(__dirname, dockerfile)
if (fs.existsSync(dockerfilePath)) {
log(`${dockerfile} trouvé`, 'green')
// Vérifier que le Dockerfile copie le bon fichier d'environnement
const content = fs.readFileSync(dockerfilePath, 'utf8')
const envName = dockerfile.replace('Dockerfile.', '')
if (content.includes(`env.${envName}`)) {
log(`${dockerfile} copie le bon fichier env.${envName}`, 'green')
} else {
log(`${dockerfile} ne copie pas env.${envName}`, 'red')
allValid = false
}
} else {
log(`${dockerfile} manquant`, 'red')
allValid = false
}
})
return allValid
}
function testViteConfig() {
log(`\n⚙️ Test de la configuration Vite`, 'cyan')
const viteConfigPath = path.join(__dirname, 'vite.config.js')
if (!fs.existsSync(viteConfigPath)) {
log(`❌ vite.config.js non trouvé`, 'red')
return false
}
log(`✅ vite.config.js trouvé`, 'green')
// Vérifier que la configuration gère les environnements
const content = fs.readFileSync(viteConfigPath, 'utf8')
if (content.includes('getEnvironmentConfig')) {
log(`✅ Configuration par environnement détectée`, 'green')
} else {
log(`⚠️ Configuration par environnement non détectée`, 'yellow')
}
if (content.includes('proxy')) {
log(`✅ Configuration proxy détectée`, 'green')
} else {
log(`⚠️ Configuration proxy non détectée`, 'yellow')
}
return true
}
function main() {
const args = process.argv.slice(2)
log(`🚀 Test des environnements Frontend LeDiscord`, 'bright')
log(`📁 Répertoire: ${__dirname}`, 'blue')
let allTestsPassed = true
if (args.length > 0) {
// Test d'un environnement spécifique
const envName = args[0]
if (['local', 'development', 'production'].includes(envName)) {
allTestsPassed = testEnvironment(envName) && allTestsPassed
} else {
log(`❌ Environnement invalide: ${envName}`, 'red')
log(` Environnements valides: local, development, production`, 'yellow')
process.exit(1)
}
} else {
// Test de tous les environnements
log(`\n🌍 Test de tous les environnements`, 'cyan')
const environments = ['local', 'development', 'production']
environments.forEach(env => {
allTestsPassed = testEnvironment(env) && allTestsPassed
})
}
// Tests généraux
allTestsPassed = testDockerfiles() && allTestsPassed
allTestsPassed = testViteConfig() && allTestsPassed
// Résumé
log(`\n📊 Résumé des tests`, 'cyan')
if (allTestsPassed) {
log(`✅ Tous les tests sont passés avec succès!`, 'green')
log(`🎉 Votre configuration frontend est prête pour la production!`, 'green')
} else {
log(`❌ Certains tests ont échoué`, 'red')
log(`🔧 Veuillez corriger les problèmes avant de continuer`, 'yellow')
process.exit(1)
}
log(`\n💡 Conseils pour éviter les problèmes de mixed content:`, 'cyan')
log(` - Assurez-vous que toutes les URLs d'un même environnement utilisent le même protocole`, 'blue')
log(` - En local: utilisez HTTP (http://localhost:*)`, 'blue')
log(` - En développement/production: utilisez HTTPS (https://*.lediscord.com)`, 'blue')
log(` - Vérifiez que VITE_API_URL et VITE_APP_URL sont cohérents`, 'blue')
}
if (require.main === module) {
main()
}

0
frontend/vite.config.js Executable file → Normal file
View File