Frontend (Angular)
Structure du Projet
front-dedicaces/
├── src/
│ ├── app/
│ │ ├── pages/ # Pages avec route
│ │ │ ├── auth/ # Authentification
│ │ │ ├── dashboard/ # Espace personnel
│ │ │ └── dedicace-view/ # Visualisation
│ │ ├── public/ # Pages publiques
│ │ │ ├── media/ # Page principale
│ │ │ ├── message-text/ # Soumission texte
│ │ │ ├── message-video/ # Soumission vidéo
│ │ │ ├── my-dedicaces/ # Mes contributions
│ │ │ ├── download/ # Téléchargement
│ │ │ └── payment/ # Paiement
│ │ ├── services/ # Services métier
│ │ ├── state/ # NGXS stores
│ │ └── gard/ # Route guards
│ ├── environments/ # Configuration
│ └── assets/ # Ressources statiques
├── angular.json # Config Angular
├── tailwind.config.js # Config Tailwind
└── package.json
Routing
Routes Principales
// Routes publiques (pas d'auth)
const publicRoutes = [
{ path: 'connexion', component: LoginComponent },
{ path: 'inscription', component: SignupComponent },
{ path: 'mot-de-passe-oublie', component: ForgotPasswordComponent },
{ path: 'confirmation-invitation', component: ConfirmInvitationComponent },
{ path: 'souvenirs/:productId', component: MediaComponent },
{ path: 'message-text/:productId', component: MessageTextComponent },
{ path: 'message-video/:productId', component: MessageVideoComponent },
];
// Routes protégées (auth requise)
const protectedRoutes = [
{
path: 'espace-personnel',
canActivate: [AuthGuard],
children: [
{ path: 'accueil', component: DashboardHomeComponent },
{ path: 'projets/creer', component: CreateProjectComponent },
{ path: 'projets/:productId', component: ProjectDetailComponent },
{ path: 'dedicaces/:id', component: DedicaceDetailComponent },
]
}
];
Guards
// auth.guard.ts
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private store: Store,
private router: Router
) {}
canActivate(): Observable<boolean> {
return this.store.select(AuthState.isAuthenticated).pipe(
tap(isAuth => {
if (!isAuth) {
this.router.navigate(['/connexion']);
}
})
);
}
}
State Management (NGXS)
Structure des States
state/
├── auth/
│ ├── auth.state.ts # État utilisateur
│ └── auth.actions.ts # Actions auth
├── products/
│ ├── products.state.ts # État événements
│ └── products.actions.ts
├── participant/
│ ├── participant.state.ts
│ └── participant.actions.ts
├── stripe/
│ └── stripe.state.ts
└── toastr/
└── toastr.state.ts
AuthState
export interface AuthStateModel {
user: User | null;
session: Session | null;
loading: boolean;
error: string | null;
}
@State<AuthStateModel>({
name: 'auth',
defaults: {
user: null,
session: null,
loading: false,
error: null
}
})
@Injectable()
export class AuthState {
@Selector()
static user(state: AuthStateModel) {
return state.user;
}
@Selector()
static isAuthenticated(state: AuthStateModel) {
return !!state.session;
}
@Action(Login)
async login(ctx: StateContext<AuthStateModel>, action: Login) {
ctx.patchState({ loading: true });
// ... logique de connexion
}
}
ProductsState
export interface ProductsStateModel {
products: Product[];
currentProduct: Product | null;
items: ProductItem[];
loading: boolean;
}
@State<ProductsStateModel>({
name: 'products',
defaults: {
products: [],
currentProduct: null,
items: [],
loading: false
}
})
export class ProductsState {
// Sélecteurs et actions pour la gestion des événements
}
Persistance
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
provideStore([AuthState, ProductsState, ParticipantState, StripeState, ToastrState]),
withNgxsStoragePlugin({
keys: ['products', 'participantMe', 'toastr']
})
]
};
Services
SupabaseService
@Injectable({ providedIn: 'root' })
export class SupabaseService {
private supabase: SupabaseClient;
constructor() {
this.supabase = createClient(
environment.supabaseUrl,
environment.supabaseKey
);
}
get client() {
return this.supabase;
}
get auth() {
return this.supabase.auth;
}
}
ProductService
Service principal (~1000 lignes) gérant :
- CRUD des événements
- Gestion des messages
- Upload de vidéos
- Invitations
@Injectable({ providedIn: 'root' })
export class ProductService {
constructor(
private supabase: SupabaseService,
private store: Store
) {}
async createProduct(data: CreateProductDto): Promise<Product> {
const { data: product, error } = await this.supabase.client
.from('product')
.insert(data)
.select()
.single();
if (error) throw error;
return product;
}
async addTextMessage(productId: string, content: string): Promise<void> {
await this.supabase.client
.from('product_items')
.insert({
product_id: productId,
message_type: 'TEXT',
message_media: content
});
}
async uploadVideo(productId: string, file: File): Promise<string> {
const path = `video_${productId}/dedicaces_${uuid()}.webm`;
const { error } = await this.supabase.client.storage
.from('video_dedicace')
.upload(path, file);
if (error) throw error;
return path;
}
}
StripeService
@Injectable({ providedIn: 'root' })
export class StripeService {
constructor(
private stripeService: NgxStripeService,
private supabase: SupabaseService
) {}
async createCheckoutSession(productId: string): Promise<void> {
const { data, error } = await this.supabase.client.functions
.invoke('stripe', {
body: { product_id: productId }
});
if (error) throw error;
await this.stripeService.redirectToCheckout({
sessionId: data.id
});
}
}
Composants Clés
MessageVideoComponent
Gère l'enregistrement et l'upload de vidéos :
@Component({
selector: 'app-message-video',
template: `
<div class="video-container">
<video #videoPreview></video>
<div class="controls">
<button (click)="startRecording()" *ngIf="!isRecording">
Enregistrer
</button>
<button (click)="stopRecording()" *ngIf="isRecording">
Arrêter
</button>
<button (click)="uploadVideo()" *ngIf="recordedBlob">
Envoyer
</button>
</div>
<div class="timer" *ngIf="isRecording">
{{ remainingTime }}s
</div>
</div>
`
})
export class MessageVideoComponent {
maxDuration = 60; // secondes
mediaRecorder: MediaRecorder;
recordedBlob: Blob;
async startRecording() {
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
this.mediaRecorder = new MediaRecorder(stream, {
mimeType: 'video/webm'
});
// Limite de 60 secondes
setTimeout(() => this.stopRecording(), this.maxDuration * 1000);
}
}
QuillEditorComponent
Configuration de l'éditeur riche :
@Component({
selector: 'app-text-editor',
template: `
<quill-editor
[(ngModel)]="content"
[modules]="editorModules"
[styles]="editorStyles"
></quill-editor>
`
})
export class TextEditorComponent {
editorModules = {
toolbar: [
['bold', 'italic', 'underline', 'strike'],
[{ 'color': [] }, { 'background': [] }],
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
[{ 'align': [] }],
['link']
]
};
}
Commandes
# Développement
npm start # Serveur dev (localhost:4200)
npm run watch # Watch mode
# Build
npm run build # Build production
npm run build:ssr # Build avec SSR
# Tests
npm test # Tests unitaires
npm run test:coverage # Avec couverture
# Qualité
npm run lint # ESLint
npm run prettier # Formatage
Configuration
Environment
// src/environments/environment.ts
export const environment = {
production: false,
supabaseUrl: 'https://xxx.supabase.co',
supabaseKey: 'eyJ...',
stripeKey: 'pk_test_...'
};
// src/environments/environment.prod.ts
export const environment = {
production: true,
supabaseUrl: 'https://xxx.supabase.co',
supabaseKey: 'eyJ...',
stripeKey: 'pk_live_...'
};
Tailwind
// tailwind.config.js
module.exports = {
content: ['./src/**/*.{html,ts}'],
theme: {
extend: {
colors: {
primary: '#...', // Couleur principale
}
}
},
plugins: [require('daisyui')],
daisyui: {
themes: ['light']
}
};