import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { RouterLink } from '@angular/router'; import { Api } from '../../core/api'; import { errorMessage } from '../../core/format'; import type { AuditStatus, CreateProjectInput, ProjectOptions, ProjectType, } from '../../core/models'; import { Icon } from '../../shared/icon'; import { JobConsole } from '../../shared/job-console'; const TYPES: { value: ProjectType; label: string; description: string }[] = [ { value: 'node', label: 'Node.js', description: 'Une app Node lancée par une commande (node start.js…).', }, { value: 'adonis', label: 'AdonisJS', description: 'Compilée (node ace build) puis lancée en production.', }, { value: 'wordpress', label: 'WordPress', description: 'Image officielle, base MariaDB dédiée.' }, { value: 'static', label: 'Site statique', description: 'Fichiers servis directement par Caddy.', }, ]; const NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,40}$/; @Component({ selector: 'app-project-new', imports: [FormsModule, RouterLink, Icon, JobConsole], templateUrl: './project-new.html', styleUrl: './project-new.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class ProjectNewPage { private api = inject(Api); protected readonly types = TYPES; protected readonly options = signal(null); protected readonly type = signal('node'); protected readonly name = signal(''); protected readonly category = signal('api'); protected readonly gitUrl = signal(''); protected readonly port = signal(3333); protected readonly command = signal(''); protected readonly env = signal(''); protected readonly createDatabase = signal(false); protected readonly addCaddy = signal(true); protected readonly spa = signal(true); /** * Null while the domain follows the name. */ protected readonly customDomain = signal(null); protected readonly submitting = signal(false); protected readonly error = signal(null); protected readonly jobId = signal(null); protected readonly jobStatus = signal(null); protected readonly domain = computed( () => this.customDomain() ?? (this.name() ? `${this.name()}.${this.options()?.baseDomain ?? ''}` : ''), ); protected readonly isApp = computed(() => this.type() === 'node' || this.type() === 'adonis'); protected readonly nameValid = computed(() => NAME_PATTERN.test(this.name())); protected readonly targetPath = computed(() => { const root = this.options()?.srvRoot ?? '/srv'; const folder = this.type() === 'static' ? 'html' : this.type() === 'wordpress' ? 'wordpress' : this.category(); return `${root}/${folder}/${this.name() || ''}`.replace(/\\/g, '/'); }); /** * What the creation will do, step by step. */ protected readonly steps = computed(() => { const steps: string[] = [`Créer le dossier ${this.targetPath()}`]; const type = this.type(); if (type === 'static') { steps.push('Y mettre une page d’attente (à remplacer par ton build)'); } else { if (this.isApp()) steps.push('Cloner le dépôt dans project/'); if (type === 'wordpress' || this.createDatabase()) { steps.push( `Créer la base MariaDB « ${(this.name() || '').replace(/-/g, '_')} » et son utilisateur`, ); } steps.push( type === 'wordpress' ? 'Écrire le .env (identifiants de la base)' : 'Écrire project/.env', ); steps.push('Écrire le docker-compose.yml (template de la procédure)'); steps.push('Lancer : docker compose up -d'); } if (this.addCaddy()) steps.push(`Ajouter ${this.domain() || ''} au Caddyfile et recharger Caddy`); return steps; }); constructor() { this.api .projectOptions() .then((options) => { this.options.set(options); this.command.set(options.defaultNodeCommand); }) .catch((error) => this.error.set(errorMessage(error))); } protected selectType(type: ProjectType) { this.type.set(type); if (type === 'wordpress') this.category.set('wordpress'); else if (this.category() === 'wordpress') this.category.set('api'); } protected async submit() { this.error.set(null); if (!this.nameValid()) { this.error.set('Le nom ne peut contenir que des minuscules, des chiffres et des tirets.'); return; } if (this.isApp() && !this.gitUrl().trim()) { this.error.set('L’URL du dépôt git est obligatoire.'); return; } const type = this.type(); const input: CreateProjectInput = { type, name: this.name(), category: type === 'wordpress' ? 'wordpress' : this.category(), createDatabase: type === 'wordpress' || (this.isApp() && this.createDatabase()), domain: this.domain(), addCaddy: this.addCaddy(), spa: this.spa(), }; if (this.isApp()) { input.gitUrl = this.gitUrl().trim(); input.port = Number(this.port()); input.env = this.env(); if (type === 'node') input.command = this.command().trim(); } this.submitting.set(true); try { const { jobId } = await this.api.createProject(input); this.jobStatus.set(null); this.jobId.set(jobId); } catch (error) { this.error.set(errorMessage(error)); } finally { this.submitting.set(false); } } protected projectLink() { const type = this.type(); const category = type === 'static' ? 'html' : type === 'wordpress' ? 'wordpress' : this.category(); return ['/projects', category, this.name()]; } }