diff --git a/assets/icons/akar-icons/location.svg b/assets/icons/akar-icons/location.svg new file mode 100644 index 000000000..39ba01fd1 --- /dev/null +++ b/assets/icons/akar-icons/location.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/oui/dot.svg b/assets/icons/oui/dot.svg new file mode 100644 index 000000000..c3b4d6d8e --- /dev/null +++ b/assets/icons/oui/dot.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/uil/calendar.svg b/assets/icons/uil/calendar.svg new file mode 100644 index 000000000..5fe736114 --- /dev/null +++ b/assets/icons/uil/calendar.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/article-default-image.svg b/assets/images/article-default-image.svg new file mode 100644 index 000000000..4cf6c6746 --- /dev/null +++ b/assets/images/article-default-image.svg @@ -0,0 +1,2 @@ + +image/svg+xml diff --git a/assets/images/forumphp-2026-vecto-light.svg b/assets/images/forumphp-2026-vecto-light.svg new file mode 100644 index 000000000..da40a5f2c --- /dev/null +++ b/assets/images/forumphp-2026-vecto-light.svg @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2026 + + + + + \ No newline at end of file diff --git a/assets/images/home-banner.webp b/assets/images/home-banner.webp new file mode 100644 index 000000000..69d5c942b Binary files /dev/null and b/assets/images/home-banner.webp differ diff --git a/assets/images/veille.webp b/assets/images/veille.webp new file mode 100644 index 000000000..67eff562f Binary files /dev/null and b/assets/images/veille.webp differ diff --git a/assets/styles/app.css b/assets/styles/app.css index 8f9b0d9fd..6b7341207 100644 --- a/assets/styles/app.css +++ b/assets/styles/app.css @@ -23,6 +23,7 @@ --color-neutre-400: #929292; --color-neutre-700: #3E3D40; + --color-ruby-200: #ed95c0; --color-ruby-400: #ed0678; --color-ruby-500: #D50068; --color-ruby-700: #B30057; @@ -38,4 +39,10 @@ /* Désactive les effets (italic, gras, etc) automatiques des polices pour prioriser les version contenues dans les polices */ font-synthesis: none; } + + .home-banner { + background-image: + linear-gradient(rgba(29, 34, 65, 0.85), rgba(29, 34, 65, 0.85)), + url('../images/home-banner.webp'); + } } diff --git a/phpstan-baseline.php b/phpstan-baseline.php index 80a9a3dd0..fc5f5be0e 100644 --- a/phpstan-baseline.php +++ b/phpstan-baseline.php @@ -4771,36 +4771,6 @@ 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Controller/Website/CompanyPublicProfile/ListAction.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'hits\' on mixed\\.$#', - 'identifier' => 'offsetAccess.nonOffsetAccessible', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Controller/Website/Global/HomeAction.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot call method search\\(\\) on mixed\\.$#', - 'identifier' => 'method.nonObject', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Controller/Website/Global/HomeAction.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method AppBundle\\\\Controller\\\\Website\\\\Global\\\\HomeAction\\:\\:doGetLatestMeetups\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Controller/Website/Global/HomeAction.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method AppBundle\\\\Controller\\\\Website\\\\Global\\\\HomeAction\\:\\:getLatestMeetups\\(\\) should return array\\ but returns mixed\\.$#', - 'identifier' => 'return.type', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Controller/Website/Global/HomeAction.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method AppBundle\\\\Controller\\\\Website\\\\Global\\\\HomeAction\\:\\:getTalkOfTheDay\\(\\) should return AppBundle\\\\Event\\\\Model\\\\Talk but returns mixed\\.$#', - 'identifier' => 'return.type', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Controller/Website/Global/HomeAction.php', -]; $ignoreErrors[] = [ 'message' => '#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#', 'identifier' => 'foreach.nonIterable', diff --git a/sources/AppBundle/Controller/Website/Global/HomeAction.php b/sources/AppBundle/Controller/Website/Global/HomeAction.php index 29443bb00..c0bb5ff62 100644 --- a/sources/AppBundle/Controller/Website/Global/HomeAction.php +++ b/sources/AppBundle/Controller/Website/Global/HomeAction.php @@ -4,94 +4,34 @@ namespace AppBundle\Controller\Website\Global; -use Afup\Site\Corporate\Feuille; -use Algolia\AlgoliaSearch\Exceptions\AlgoliaException; -use Algolia\AlgoliaSearch\SearchClient; -use AppBundle\Event\Entity\Meetup; -use AppBundle\Event\Model\Repository\TalkRepository; -use AppBundle\Event\Model\Talk; +use AppBundle\Event\Entity\Repository\MeetupRepository; +use AppBundle\Event\Model\Repository\EventRepository; +use AppBundle\Site\ArticleImageStorage; use AppBundle\Site\Entity\Repository\ArticleRepository; -use AppBundle\Site\Entity\Repository\FeuilleRepository; use AppBundle\Twig\ViewRenderer; -use Psr\Cache\CacheItemPoolInterface; -use Psr\Cache\InvalidArgumentException; -use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; -use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\HttpFoundation\Response; final class HomeAction extends AbstractController { - public const int MAX_MEETUPS = 10; - public function __construct( private readonly ViewRenderer $view, - private readonly LoggerInterface $logger, - private readonly CacheItemPoolInterface $cache, - private readonly SearchClient $client, - private readonly TalkRepository $talkRepository, - #[Autowire(env: 'HOME_ALGOLIA_ENABLED')] - private readonly bool $homeAlgoliaEnabled, private readonly ArticleRepository $articleRepository, - private readonly FeuilleRepository $feuilleRepository, + private readonly MeetupRepository $meetupRepository, + private readonly EventRepository $eventRepository, + private readonly ArticleImageStorage $articleImageStorage, ) {} public function __invoke(): Response { - $derniers_articles = $this->articleRepository->findListForHome(); - - $enfants = $this->feuilleRepository->getFeuillesEnfant(Feuille::ID_FEUILLE_COLONNE_DROITE); - - $premiereFeuille = array_shift($enfants); - $deuxiemeFeuille = array_shift($enfants); + $articles = $this->articleRepository->findListForHome(); + $meetups = $this->meetupRepository->findNextEvents(6); return $this->view->render('site/home.html.twig', [ - 'actualites' => $derniers_articles, - 'meetups' => $this->getLatestMeetups(), - 'premiere_feuille' => $premiereFeuille, - 'deuxieme_feuille' => $deuxiemeFeuille, - 'autres_feuilles' => $enfants, - 'talk' => $deuxiemeFeuille ?? $this->getTalkOfTheDay(), + 'articles' => $articles, + 'meetups' => $meetups, + 'lastArticleImageUrl' => $this->articleImageStorage->getUrl($articles[0]) ?? 'images/article-default-image.svg', + 'currentEvent' => $this->eventRepository->getMostRecentEvent(), ]); } - - protected function getTalkOfTheDay(): Talk - { - return $this->talkRepository->getTalkOfTheDay(new \DateTime()); - } - - /** - * @return array - */ - protected function getLatestMeetups(): array - { - if (!$this->homeAlgoliaEnabled) { - return []; - } - - $cacheKey = 'home_algolia_meetups_doctrine'; - - try { - $cacheItem = $this->cache->getItem($cacheKey); - if (!$cacheItem->isHit()) { - $cacheItem->expiresAfter(new \DateInterval('P1D')); - $cacheItem->set($this->doGetLatestMeetups()); - $this->cache->save($cacheItem); - } - - return $cacheItem->get(); - } catch (AlgoliaException|InvalidArgumentException $e) { - $this->logger->error($e->getMessage()); - return []; - } - } - - private function doGetLatestMeetups() - { - $algolia = $this->client; - $index = $algolia->initIndex('afup_meetups'); - $results = $index->search('', ['hitsPerPage' => self::MAX_MEETUPS]); - - return $results['hits']; - } } diff --git a/sources/AppBundle/Event/Entity/Repository/MeetupRepository.php b/sources/AppBundle/Event/Entity/Repository/MeetupRepository.php index 3f3f9295a..fb77f78f8 100644 --- a/sources/AppBundle/Event/Entity/Repository/MeetupRepository.php +++ b/sources/AppBundle/Event/Entity/Repository/MeetupRepository.php @@ -49,4 +49,16 @@ public function findAllForAntenne(Antenne $antenne): array ->getQuery() ->getResult(); } + + /** + * @return array + */ + public function findNextEvents(int $quantity): array + { + return $this->createQueryBuilder('m') + ->orderBy('m.date', 'DESC') + ->setMaxResults($quantity) + ->getQuery() + ->getResult(); + } } diff --git a/sources/AppBundle/Site/Entity/Repository/ArticleRepository.php b/sources/AppBundle/Site/Entity/Repository/ArticleRepository.php index 799b565ea..71626e702 100644 --- a/sources/AppBundle/Site/Entity/Repository/ArticleRepository.php +++ b/sources/AppBundle/Site/Entity/Repository/ArticleRepository.php @@ -181,7 +181,7 @@ public function findListForHome(): array ->andWhere('r.id <> ' . Rubrique::ID_RUBRIQUE_ANTENNES) ->andWhere('r.id <> ' . Rubrique::ID_RUBRIQUE_NOS_ACTIONS) ->orderBy('a.datePublication', 'DESC') - ->setMaxResults(5) + ->setMaxResults(6) ->getQuery() ->getResult(); } diff --git a/templates/components/Button.html.twig b/templates/components/Button.html.twig index a20a256b2..8d5189abe 100644 --- a/templates/components/Button.html.twig +++ b/templates/components/Button.html.twig @@ -1,4 +1,4 @@ -{# @prop variant 'primary'|'secondary'|'tertiary' Quel genre de bouton #} +{# @prop variant 'primary'|'secondary'|'secondary-outline'|'tertiary' Quel genre de bouton #} {# @block content Contenu du bouton #} {# @prop as 'button'|'a' Tag HTML #} {%- props variant = 'primary', as = 'button' -%} @@ -8,6 +8,7 @@ variant: { primary: 'bg-ruby-500 text-white hover:bg-ruby-700', secondary: 'bg-white text-neutre-700 border-neutre-300 hover:bg-afup-700 hover:text-white hover:border-afup-700', + 'secondary-outline': 'bg-transparent text-white border-white border-2 hover:bg-afup-200', tertiary: 'bg-afup-100 text-afup-500 hover:bg-afup-500 hover:text-white', }, }, diff --git a/templates/components/Card/Subtitle.html.twig b/templates/components/Card/Subtitle.html.twig new file mode 100644 index 000000000..72fb812a8 --- /dev/null +++ b/templates/components/Card/Subtitle.html.twig @@ -0,0 +1,7 @@ +{# @block content Contenu du titre #} +{%- set style = html_cva( + base: "text-base font-light text-afup-800 border-b border-neutre-300 pb-2 mb-5" +) -%} +

+ {%- block content %}{% endblock -%} +

diff --git a/templates/components/Card/Title.html.twig b/templates/components/Card/Title.html.twig index 9c285d57a..02871b35b 100644 --- a/templates/components/Card/Title.html.twig +++ b/templates/components/Card/Title.html.twig @@ -1,15 +1,20 @@ {# @prop variant 'default'|'form' Quel genre de titre #} +{# @prop size 'default'|'big' Taille du titre #} {# @block content Contenu du titre #} -{%- props type = 'default' -%} +{%- props type = 'default', size = 'default' -%} {%- set style = html_cva( - base: "font-titre font-light text-lg mb-5", + base: "font-titre font-light mb-5", variants: { type: { default: "text-afup-800", form: "text-afup-500", - } + }, + size: { + default: "text-lg", + big: "text-2xl", + }, } ) -%} -

+

{%- block content %}{% endblock -%}

diff --git a/templates/components/MainFooter.html.twig b/templates/components/MainFooter.html.twig index 77931dc11..fbbaee322 100644 --- a/templates/components/MainFooter.html.twig +++ b/templates/components/MainFooter.html.twig @@ -17,19 +17,19 @@ diff --git a/templates/components/Time/DayAndMonth.html.twig b/templates/components/Time/DayAndMonth.html.twig new file mode 100644 index 000000000..dc071ea0d --- /dev/null +++ b/templates/components/Time/DayAndMonth.html.twig @@ -0,0 +1,15 @@ +{% props date, variant %} +{# @prop variant 'dark'|'light' Le type d'alerte #} +{%- set style = html_cva( + base: "text-white text-sm flex items-center px-2 py-4 rounded-lg", + variants: { + variant: { + dark: 'bg-afup-700', + light: 'bg-afup-500', + }, + }, +) -%} + diff --git a/templates/components/Time/Format.html.twig b/templates/components/Time/Format.html.twig new file mode 100644 index 000000000..fe713710d --- /dev/null +++ b/templates/components/Time/Format.html.twig @@ -0,0 +1,4 @@ +{% props date, format %} + diff --git a/templates/site/home.html.twig b/templates/site/home.html.twig index f64e2ba07..1e32e75b9 100644 --- a/templates/site/home.html.twig +++ b/templates/site/home.html.twig @@ -1,4 +1,4 @@ -{% extends 'site/base.html.twig' %} +{% extends 'layouts/site.html.twig' %} {% block metas %} {{ parent() }} @@ -23,118 +23,204 @@ {% endblock %} -{% import _self as home_macros %} +{% block content %} +
+
+

+ AFUP : Association Française
des Utilisateurs de PHP +

+ +

+ Apéros, rendez-vous et cycle de conférences, l'AFUP est au cœur de la communauté PHP depuis 2000.
+ L'AFUP vise à favoriser l'échange d'expertises et la diffusion des connaissances auprès de la communauté. +

+
+
+ +
+
+

+ Actualités +

+ +
+ + + + Dernières actualités + + +
+ -
- {% if premiere_feuille %} - {{ home_macros.home_feuille(premiere_feuille) }} - {% endif %} -
-
- -
-
-

Dans les antennes

- +
+ + {% endfor %} - Voir plus + + Voir toutes les actualités +
+ +
+
+ +
+

+ {{ currentEvent.title }} +

+ +
+
+ Logo {{ currentEvent.title }} +
+
+

+ Participez au {{ currentEvent.title }} +

+

+ Le rendez-vous incontournable de la communauté PHP en France. Rejoignez-nous les 8 et 9 octobre 2026 + pour deux jours de conférences, d'ateliers et de networking. +

+
+
+
+ +
+

8 & 9 octobre 2026

+
-
- {% if deuxieme_feuille %} - {{ home_macros.home_feuille(deuxieme_feuille) }} - {% elseif talk %} -

Le talk du jour

- -
- {{ talk.getTitle }} -
- - {% endif %} +
+
+ +
+

+ Hotel New York - The Art of Marvel,
+ Disneyland Paris +

+
+
+
+ + Réserver ma place + + + + Voir le programme +
+
+
+ +
+ + Dans les antennes + Meetups à travers la France + +
+ {% for meetup in meetups %} + +
+ + +
+

+ {{ office_name(meetup.codeAntenne) }} +

+

+ {{ meetup.titre }} +

+
+
+
+ {% endfor %} + + + Voir touts les meetups + +
+
- {% set autres_feuilles_container_open = 0 %} - {% for feuille in autres_feuilles %} - {% if loop.index is odd %} - {% set autres_feuilles_container_open = 1 %} -
- {% endif %} + + La veille de l'AFUP + Newsletter technique pour les devs PHP -
- {{ home_macros.home_feuille(feuille) }} -
+
+ Veille - {% if loop.index is even %} -
- {% endif %} - {% endfor %} - {% if autres_feuilles_container_open %} -
- {% endif %} +

+ Membres AFUP, profitez du meilleur de PHP et soyez à jour sur son actualité.
+
+ Abonnez-vous à notre newsletter La Veille de l'AFUP. Progrès du langage, conseils de spécialistes, + nouvelles versions, l'AFUP fait la veille pour vous et vous l'envoie par mail. +

-
- + + En savoir plus + + + + + {% endblock %} diff --git a/tests/behat/features/PublicSite/Home.feature b/tests/behat/features/PublicSite/Home.feature index ff9d3bfbb..b0c8fdca7 100644 --- a/tests/behat/features/PublicSite/Home.feature +++ b/tests/behat/features/PublicSite/Home.feature @@ -2,18 +2,32 @@ Feature: Site Public - Home @reloadDbWithTestData Scenario: On voit bien toute la page, même le footer - Given I am on the homepage - Then I should see "© AFUP 2003-" + Given the current date is "2026-01-01" + And I am on the homepage + Then I should see "© AFUP 2000-2026" @reloadDbWithTestData - Scenario: Sur la home a a bien la partie "Talk du jour d'affichée" + Scenario: Sur la home on a bien un lien vers le dernier article affiché Given I am on the homepage - Then I should see "Le talk du jour" + + # Article à la une + Then I should see "Un article en Markdown" + And I should see "30 octobre 2025" + And I should see "Un peu de text avec de la mise en forme" + + # Article dans la liste + Then I should see "Les vidéos des talks du Forum PHP 2018 sont disponibles" + And I should see "14/11" + And I should not see "venez nous voir en octobre au Forum PHP 2019 ou en mai à l'AFUP Day" + + # Affichage de la page de l'article à la une + When I follow "Lire l'article complet" + Then I should see "Un article en Markdown" + And I should see "Lorem ipsum dolor sit amet" @reloadDbWithTestData - Scenario: Sur la home a a bien un lien vers le dernier article affiché + Scenario: Affichage des derniers meetups Given I am on the homepage - Then I should see "Les vidéos des talks du Forum PHP 2018 sont disponibles" - When I follow "14/11" - Then I should see "14 novembre 2018" - And I should see "venez nous voir en octobre au Forum PHP 2019 ou en mai à l'AFUP Day" + Then I should see "Le super meetup" + And I should see "Lyon" + And I should see "08/11"