Aller au contenu principal

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']
}
};