Commit bfb4036e authored by Kourser's avatar Kourser
Browse files

Presse : illustrer un article depuis l'onglet Communication

Le sélecteur d'image n'existait que dans le compositeur d'injects. Publier
directement depuis l'onglet Communication ne laissait aucun moyen de joindre
une illustration, alors que le rendu de l'article savait déjà en afficher une.

Le sélecteur n'apparaît que là où un téléversement est possible : une image
appartient à un exercice, et l'espace joueur ne publie pas d'article.

Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
parent 766eca67
Loading
Loading
Loading
Loading
+1 −0
Changes for apps/web/src/Exercises.tsx: 1 added line, 0 removed lines.
Original line number Diff line number Diff line
@@ -513,6 +513,7 @@ function ExerciseDetail({ id, onBack }: { id: string; onBack: () => void }): JSX
                  <News
                    load={() => api.listNews(id)}
                    onPublish={(input) => api.createNews(id, input)}
                    uploadImage={(file) => api.uploadImage(id, file)}
                    joinExerciseId={id}
                  />
                </div>
+27 −3
Changes for apps/web/src/News.tsx: 27 added lines, 3 removed lines.
Original line number Diff line number Diff line
import { useCallback, useEffect, useState, type FormEvent } from 'react';
import type { ApiNewsArticle, NewsOutlet } from '@enkadenn/shared';
import { ApiError } from './api';
import { ApiError, type UploadedImage } from './api';
import { ImageAttach } from './ImageAttach';
import { connectSocket, formatClock, onReconnect, RT } from './socket';
import { useT } from './i18n';

@@ -80,6 +81,7 @@ function Wordmark({ outlet }: { outlet: NewsOutlet }): JSX.Element {
export function News({
  load,
  onPublish,
  uploadImage,
  joinExerciseId,
}: {
  load: () => Promise<ApiNewsArticle[]>;
@@ -89,7 +91,13 @@ export function News({
    category?: string;
    source: string;
    body: string;
    imageId?: string;
  }) => Promise<unknown>;
  /**
   * Exercice d'accueil du téléversement. Sans lui, le formulaire ne propose pas
   * d'illustration : une image doit appartenir à un exercice.
   */
  uploadImage?: (file: File) => Promise<UploadedImage>;
  joinExerciseId?: string;
}): JSX.Element {
  const { t } = useT();
@@ -126,7 +134,7 @@ export function News({

  return (
    <div className="news">
      {onPublish && <PublishArticle onPublish={onPublish} />}
      {onPublish && <PublishArticle onPublish={onPublish} uploadImage={uploadImage} />}
      {error && <p className="err">{error}</p>}

      {open ? (
@@ -205,6 +213,7 @@ function NewsSite({ article: a, onBack }: { article: ApiNewsArticle; onBack: ()

function PublishArticle({
  onPublish,
  uploadImage,
}: {
  onPublish: (input: {
    outlet?: NewsOutlet;
@@ -212,13 +221,17 @@ function PublishArticle({
    category?: string;
    source: string;
    body: string;
    imageId?: string;
  }) => Promise<unknown>;
  uploadImage?: (file: File) => Promise<UploadedImage>;
}): JSX.Element {
  const [outlet, setOutlet] = useState<NewsOutlet>('GENERIC');
  const [headline, setHeadline] = useState('');
  const [category, setCategory] = useState('');
  const [source, setSource] = useState('');
  const [body, setBody] = useState('');
  /** Illustration de l'article, facultative. */
  const [image, setImage] = useState<UploadedImage | null>(null);
  const [error, setError] = useState<string | null>(null);

  function chooseOutlet(o: NewsOutlet): void {
@@ -231,9 +244,17 @@ function PublishArticle({
    e.preventDefault();
    setError(null);
    try {
      await onPublish({ outlet, headline, category: category || undefined, source, body });
      await onPublish({
        outlet,
        headline,
        category: category || undefined,
        source,
        body,
        imageId: image?.id,
      });
      setHeadline('');
      setBody('');
      setImage(null);
    } catch (err) {
      setError(err instanceof ApiError ? err.message : 'Publication impossible');
    }
@@ -258,6 +279,9 @@ function PublishArticle({
        <textarea placeholder="Corps de l'article…" value={body} onChange={(e) => setBody(e.target.value)} rows={2} required />
      </div>
      <div className="row">
        {/* Illustration : proposée seulement là où un téléversement est possible —
            une image appartient à un exercice, l'espace joueur n'en publie pas. */}
        {uploadImage && <ImageAttach value={image} onChange={setImage} upload={uploadImage} />}
        <button className="btn primary" type="submit">
          Publier
        </button>
+8 −1
Changes for apps/web/src/api.ts: 8 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -601,7 +601,14 @@ export const api = {
  listNews: (exerciseId: string) => request<ApiNewsArticle[]>(`/exercises/${exerciseId}/news`),
  createNews: (
    exerciseId: string,
    input: { outlet?: NewsOutlet; headline: string; category?: string; source: string; body: string },
    input: {
      outlet?: NewsOutlet;
      headline: string;
      category?: string;
      source: string;
      body: string;
      imageId?: string;
    },
  ) =>
    request<ApiNewsArticle>(`/exercises/${exerciseId}/news`, {
      method: 'POST',