# Schoool — CLAUDE.md

## Progetto
Sito Drupal 10. Piattaforma formativa (corsi, risorse, blog).

## Ambiente di sviluppo
Il progetto gira in **DDEV** (Docker). Il PHP usato da drush e composer è quello del container, non quello dell'host.

```bash
# Comandi drush: SEMPRE tramite DDEV
ddev drush cr
ddev drush en <modulo> -y
ddev drush config:import --partial -y

# Composer: eseguito sull'host (PHP host deve funzionare)
composer require drupal/<pacchetto>
```

**Nota PHP host:** se `composer` fallisce con errore `libicuio.74.dylib`, eseguire `brew reinstall php` per riallineare PHP all'icu4c corrente. Il PHP del container DDEV non è affetto da questo problema.

## Tema
- Nome: `tailwindcss` (tema contribuito adattato)
- Percorso: `web/themes/custom/tailwindcss/`
- Base theme: `stable` (Bootstrap Barrio è disattivato)
- Admin theme: Gin
- CSS framework: Tailwind CSS v3

## Componenti SDC
I componenti header e footer sono Single Directory Components (SDC) di Drupal.
- `components/header/` — sticky header con logo SVG, nav desktop, CTA button, drawer mobile
- `components/footer/` — footer con newsletter, sitemap, sub-footer

### Regola full-bleed
**Solo il footer** usa la tecnica full-bleed (`width: 100vw; position: relative; left: 50%; margin-left: -50vw`).
`html { overflow-x: hidden }` è impostato in `css/components/page.css`.

**L'header NON usa full-bleed** — usa `width: 100%`. Motivo: è sempre fuori dai wrapper con padding/margin, e il full-bleed causava sovrapposizione con la sidebar dell'admin theme Gin.

### Regola posizionamento header/footer nei template
I componenti SDC header e footer devono stare **FUORI** dal wrapper `<div{{ attributes.addClass(classes) }}>` che riceve la classe `m-16` da `tailwindcss_preprocess_page()` quando l'utente è loggato.

### Sistema padding mobile (footer)
- `--footer-padding-inline: 1rem` su mobile, `0` su desktop (≥ 1024px)

## Design system
Ispirato al blueprint visivo jonas.io. Classi e token definiti in:
- `css/client-overrides.css` — token CSS (colori, font, spacing)
- `css/components/design-system.css` — utility classes (container, sezioni, bottoni, card, layout)

### Token principali
```css
--client-color-primary: #37a169    /* brand green */
--client-color-heading: #1a1a1a
--client-color-text: #333333
--client-color-muted: #888888
--client-color-bg-alt: #f3f3f0     /* sezioni alternate */
--client-container-max: 1100px
--client-section-py: 6rem
```

### Classi container e sezioni
```html
<div class="site-container">          <!-- max 1100px, px-6/lg:px-12 -->
<section class="section">             <!-- py-24 -->
<section class="section section-alt"> <!-- py-24 + bg #f3f3f0 -->
```

### Bottoni
```html
<a class="btn-site btn-primary">  <!-- verde, rounded-full -->
<a class="btn-site btn-ghost">    <!-- bordo, trasparente -->
```

## Struttura page templates principali
- `page--front.html.twig` — home page (hero, feature sections dinamiche via block_content, testimonianze, CTA)
- `page--node.html.twig` — nodi generici; `page.header` wrapped in `site-container text-center`
- `page--corso.html.twig` — nodo corso: mostra lista lezioni sopra il contenuto (se presenti)
- `page--corsi.html.twig` — lista corsi (route `view.corsi.page_1`)
- `page--articoli.html.twig` — lista articoli (route `view.articoli.page_1`)
- `page--blog.html.twig` — blog
- `page--landing.html.twig` — landing page
- `page--area-studente.html.twig` — area studente (route `schoool_corsi.area_studente`)
- `page--dashboard--studente.html.twig` — dashboard studente (`/dashboard/studente`)
- `page--dashboard--editor.html.twig` — dashboard editor (`/dashboard/editor`) — lista corsi
- `page--dashboard--editor--corso.html.twig` — gestione lezioni corso (`/dashboard/editor/corso/{nid}`)
- `page--dashboard--admin.html.twig` — dashboard admin (`/dashboard/admin`)
- `page--dashboard--editor--iscritti.html.twig` — iscritti ai corsi dell'editor
- `page--dashboard--editor--ordini.html.twig` — ordini per i corsi dell'editor
- `page.html.twig` — default

**Percorso template:** `templates/layout/page/`

**Theme suggestions views** in `tailwindcss_theme_suggestions_page_alter()`:
```php
'view.corsi.page_1'    → 'page__corsi'
'view.articoli.page_1' → 'page__articoli'
```

## Sistema corsi (Commerce + License)

### Architettura
```
corso (node) ← referenziato da → commerce_product tipo "corso"
                                        ↓ variation con trait commerce_license
                                  ORDER COMPLETED
                                        ↓
                              CorsoAccess license (plugin corso_access)
                              campo field_licensed_corso → node.corso
                                        ↓
                    lezione (node) ← hook_node_access verifica licenza
                                        ↓
                              /area-studente/{corso} — dashboard
```

### Content types
- **`corso`** — immagine copertina, durata (text), instructor (text), body
- **`lezione`** — `field_lezione_corso` (ref→corso, required), `field_lezione_video` (link), `field_lezione_peso` (integer, ordine)

### Commerce
- Product type: `corso` / variation type: `corso`
- Variation type usa il trait `commerce_license` (aggiunge `license_type` e `license_expiration`)
- **ATTENZIONE:** dopo config import, il trait va installato esplicitamente via PHP:
  ```php
  $tm = Drupal::service('plugin.manager.commerce_entity_trait');
  $tm->installTrait($tm->createInstance('commerce_license'), 'commerce_product_variation', 'corso');
  ```
- Campo prodotto: `field_product_corso_ref` → node.corso

### Modulo custom `schoool_corsi`
**Percorso:** `web/modules/custom/schoool_corsi/`

- **`CorsoAccess`** plugin (`id = "corso_access"`) — `src/Plugin/Commerce/LicenseType/CorsoAccess.php`
  - Bundle field sulla licenza: `field_licensed_corso` → node.corso
  - `grantLicense()` / `revokeLicense()` = noop (accesso dinamico)
- **`hook_node_access()`** in `schoool_corsi.module` — protegge nodi `lezione` (op=view, non admin)
  - Verifica: licenza attiva con `type=corso_access`, `state=active`, `field_licensed_corso=corso_nid`
- **`AreaStudenteController`** — route `/area-studente/{node}` (node = corso)
  - Access check via licenza attiva
  - Render: lista lezioni ordinate per `field_lezione_peso` + view `commerce_user_orders`
- **`hook_theme_suggestions_page_alter()`** — aggiunge suggestion `page__area_studente` e tutte le dashboard
- **`StudenteDashboardController`** — `/dashboard/studente`, carica tutte le licenze `corso_access` dell'utente (qualsiasi stato)
- **`EditorDashboardController`**:
  - `view()` → `/dashboard/editor` — lista corsi pulita, quick links (Nuovo Corso, Iscritti, Ordini)
  - `corsoDetail(NodeInterface $node)` → `/dashboard/editor/corso/{nid}` — lezioni con tabledrag + tasto Salva
  - `iscritti()`, `ordini()`, `reorder()` — pagine secondarie
- **`AdminDashboardController`** — extends Editor, theme `admin_dashboard`, aggiunge quick links globali
- **Permessi**: `access editor dashboard`, `access admin dashboard`, `reorder lezioni`
- **Tabledrag**: `drupalSettings.tableDrag` generato dal controller PHP; chiave = `name` dell'input (`lezione_peso_{nid}`); `<tr>` classe `draggable`; JS custom gestisce tasto "Salva ordine" → AJAX
- **Pre-popolazione lezione**: `hook_form_node_lezione_form_alter` legge `?field_lezione_corso[0][target_id]` e imposta `#default_value` — pattern per pre-popolare entity_reference da URL
- **Dashboard URL in header**: `dashboard_url` calcolato in `tailwindcss_preprocess_page()`, passato all'SDC header

### Header — Area Corsi
- Desktop: `.sdc-header__dashboard-link` nel nav, `display: inline-flex` (≥1024px)
- Mobile: `.sdc-header__dashboard-bar` — barra verde dentro `<header>` dopo `.sdc-header__inner`, visibile direttamente senza aprire hamburger; nascosta a ≥1024px

### Config import con nuovi moduli
Quando si installa un nuovo modulo via `ddev drush en`:
1. Aggiornare `config/sync/core.extension.yml` (aggiungere il modulo)
2. Copiare le config del modulo da DB a sync via PHP eval:
   ```php
   $sync->write($name, $active->read($name));
   ```
3. `ddev drush config:import --partial -y`

## Librerie principali
```yaml
global-styling:
  css:
    component:
      css/components/design-system.css: { weight: -10 }
      dist/custom.css: { weight: 2 }
      css/client-overrides.css: { weight: 99 }
    base:
      dist/tailwind.css: {}
global-interactions:
  js: js/header-drawer.js
```

## Views grid (corsi / articoli)

Il tema `stable` NON aggiunge `view-id-*` né `view-content` al wrapper delle view. Soluzione obbligatoria: override di `views-view-unformatted--[view_id].html.twig` che aggiunge un wrapper con classe nota.

```
templates/views/views-view-unformatted--corsi.html.twig    → <div class="corsi-grid">
templates/views/views-view-unformatted--articoli.html.twig → <div class="articoli-grid">
```

CSS in `design-system.css`: griglia responsive con `.views-row { display: contents }`.
Le card (`.corso-card`, `.article-card`) devono avere `width: 100%` esplicito.

### Card teaser articoli — pitfall
- Image wrap: usare **`<div>`**, NON `<a>` — l'image formatter Drupal aggiunge il suo `<a>` creando nesting invalido
- Condizione immagine: `{% if node.field_immagine is not empty %}` — `.entity` su FieldItemList vuoto è truthy → immagine rotta
- Primo tag: accedere tramite `node.field_tags.0.entity.label`, non `content.field_tags`
- `{{ product.field_image }}` / `{{ content.field_immagine }}` usano il formatter del display → può essere `entity_reference_label` (solo testo). Accedere sempre direttamente all'entità media

### Stable theme — CSS pitfall
Il tema `stable` non aggiunge classi CSS ai field wrapper (niente `.field__item`, `.field`).
Il selettore corretto per il testo dentro un field è `> div` (figlio diretto).

## Campi immagine (media)

I campi immagine sono stati migrati a `entity_reference` → `media` (media type `image`, source field `field_media_image`).

- **commerce_product corso**: `field_image` (entity_reference → media)
- **node article**: `field_immagine` (entity_reference → media)

**Pattern Twig (richiede `twig_tweak` installato):**
```twig
{% if node.field_immagine is not empty %}
  {% set media = node.field_immagine.entity %}
  {% if media %}
    {% set img = media.field_media_image %}
    <img src="{{ img.entity.uri.value|image_style('large') }}" alt="{{ img.alt }}" />
  {% endif %}
{% endif %}
```

**PITFALL**: `{% if node.field_xxx.entity %}` è truthy anche a campo vuoto → broken img. Usare sempre `is not empty`.

## Feature Sections — home page dinamica

Le sezioni 01/02/03 della home page sono gestite via **block content type `feature_section`**.

### Campi del tipo
- `field_fs_heading` — titolo (string, obbligatorio)
- `field_fs_body` — testo (text_long)
- `field_fs_cta_primary` — link con etichetta (obbligatorio)
- `field_fs_cta_secondary` — link con etichetta (opzionale)
- `field_immagine` — entity_reference → media (storage condiviso con altri bundle)
- `field_fs_peso` — integer, ordine di visualizzazione

### Caricamento in preprocess
`tailwindcss_preprocess_page()` carica tutti i `feature_section` ordinati per `field_fs_peso` ASC e li passa come `$variables['feature_sections']`.

### Regola alternanza immagine
Il loop in `page--front.html.twig` usa `loop.index`:
- Dispari → `class="feature-row feature-row-reverse"` → immagine **sinistra**
- Pari → `class="feature-row"` → immagine **destra**

**ATTENZIONE**: `feature-row-reverse` NON ha `display:flex` da solo — va sempre combinato con `feature-row`. Usare sempre entrambe le classi insieme per il layout invertito.

### Gestione contenuti
Creare/modificare blocchi su `/block/add/feature_section`. Ordinare tramite il campo `Ordine (peso)`.

## Article node template
`templates/layout/node/node--article.html.twig` — layout:
1. Hero image full-width (`.article-single__hero`) — campo `field_immagine` (media)
2. Reading area centrata max 720px (`.article-single__reading`)
   - `.article-single__meta` — autore/data italic
   - `.article-single__body` — font 1.125rem, line-height 1.85
   - `.article-single__tags` — tags inline italic (via `field--node--field-tags.html.twig`)

## Header — cart block e user menu
- **Cart block**: passato via `tailwindcss_preprocess_page()` con check `moduleExists('commerce_cart')`
- Icona nera: `tailwindcss_preprocess_commerce_cart_block()` sovrascrive `$variables['icon']`
- Template override Commerce: `templates/commerce/commerce-cart-block.html.twig` — mostra solo `{{ count }}` (numero), nasconde se 0
- Su mobile: `.cart-block--summary__count` e `.sdc-header__user-label` nascosti (solo icone); ripristinati su desktop (≥1024px)
- **User menu** (`.sdc-header__user`): link a `/user/login` (anonimo) o `/user` (loggato), sempre visibile. Usa `{{ logged_in }}` da contesto Twig.

## Lezioni corso (page--corso.html.twig)

Il template `page--corso.html.twig` mostra le lezioni del corso in cima alla pagina, sopra `page.content`.

### Come funziona
`tailwindcss_preprocess_page()` carica le lezioni solo quando il nodo corrente è di tipo `corso`:
```php
$lezione_ids = \Drupal::entityQuery('node')
  ->condition('type', 'lezione')
  ->condition('field_lezione_corso', $route_node->id())
  ->condition('status', 1)
  ->sort('field_lezione_peso', 'ASC')
  ->accessCheck(FALSE)
  ->execute();
$variables['lezioni'] = $lezione_ids ? array_values(...loadMultiple($lezione_ids)) : [];
```

Il Twig mostra la sezione solo se `lezioni|length > 0`. Ogni riga:
- Numero circolare verde (`.corso-lezioni-num`)
- Link al nodo lezione (`.corso-lezioni-title`)
- Icona play SVG se `field_lezione_video is not empty`

CSS: classi `.corso-lezioni-*` in `css/components/design-system.css`.

## CI/CD — Deploy produzione

### Workflow
- File: `.github/workflows/deploy-production.yml`
- Trigger: **`workflow_dispatch`** (manuale da GitHub UI, solo da branch `main`)
- Altri workflow: `ci.yml`, `build-staging-artifact.yml`

### Repo GitHub
`https://github.com/Schoool-org/schoool.git` (organizzazione Team)

### Secrets richiesti
| Secret | Valore |
|---|---|
| `PROD_SSH_HOST` | hostname server |
| `PROD_SSH_PORT` | porta SSH |
| `PROD_SSH_USER` | utente SSH |
| `PROD_SSH_PRIVATE_KEY` | chiave privata ed25519 |
| `PROD_SSH_KNOWN_HOSTS` | known_hosts del server |
| `PROD_DRUSH_URI` | URI produzione per drush |
| `PROD_DEPLOY_PATH` | path relativo alla home, es. `public_html/schoool` |
| `PROD_PHP_BIN` | `/opt/cpanel/ea-php83/root/usr/bin/php` |

### Struttura server (setup iniziale one-time)
```bash
mkdir -p ~/public_html/schoool/shared/web/sites/default/files
mkdir -p ~/public_html/schoool/shared/web/sites/default/private
# Caricare manualmente settings.php di produzione:
~/public_html/schoool/shared/web/sites/default/settings.php
```

### Note server
- Hosting InMotion/cPanel — home in `$HOME`, web root in `$HOME/public_html/`
- PHP default del server: 8.1 → usare `PROD_PHP_BIN` per PHP 8.3
- `DEPLOY_PATH = $HOME/${PROD_DEPLOY_PATH}` (espansione server-side)
- Il workflow usa releases con symlink (`current` → release più recente), mantiene ultime 5
- **Document root cPanel**: `public_html/schoool/current/web`
- **PHP handler cPanel** (Apache Handlers): `application/x-httpd-ea-php83` per `.php .php8 .phtml`

### Pitfall symlink `current`
`ln -sfn TARGET DIR` su Linux: se `DIR` è già una directory reale, crea il link dentro di essa invece di sostituirla. Il workflow include `rm -rf "$DEPLOY_PATH/current"` prima di `ln -sfn` per evitarlo.

### Procedura primo deploy (o re-deploy da zero)
```bash
# 1. Dump DB da DDEV locale
ddev drush sql-dump --gzip > /tmp/db.sql.gz
scp /tmp/db.sql.gz user@server:~/

# 2. Sul server (da $DEPLOY_PATH/current)
zcat ~/db.sql.gz | $PHP_BIN vendor/bin/drush --root=web sql-cli
$PHP_BIN vendor/bin/drush --root=web updatedb -y
$PHP_BIN vendor/bin/drush --root=web config:import -y
$PHP_BIN vendor/bin/drush --root=web cache:rebuild
```

### Sync files (PhpStorm)
Deployment → Sync with Deployed. Mapping:
- Local: `web/sites/default/files/`
- Remote: `/public_html/schoool/shared/web/sites/default/files/`

## Comandi utili
```bash
# Compilare Tailwind (da web/themes/custom/tailwindcss/)
npm run build   # produzione, minified
npm run dev     # watch mode

# Pulire cache Drupal (sempre via DDEV)
ddev drush cr
# oppure: Admin → Configuration → Performance → Clear all caches
```
