Add complete Acai development documentation
- CLAUDE.md: expanded with critical rules, hook syntax, DB conventions, web-base endpoint - docs/modular-system.md: modules, general sections, global vars, multiv2 - docs/builder-fields.md: all field types, c-if/c-for/c-class, c-form, built-in components - docs/twig-filters.md: get, hook, module, queryDB, imagec, translate, raw, etc. - docs/hooks-and-api.md: PHP hooks, CmsApi CRUD, table schemas, field formats - docs/css-js-conventions.md: Tailwind, BEM, CSS vars, Vue 3 integration, CmsApi JS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
51
CLAUDE.md
51
CLAUDE.md
@@ -6,7 +6,7 @@ This is an Acai CMS website project. Follow these instructions when working with
|
|||||||
|
|
||||||
- The site runs in Docker, typically at **http://localhost:8080**
|
- The site runs in Docker, typically at **http://localhost:8080**
|
||||||
- You can make HTTP requests to test pages, APIs, or form submissions
|
- You can make HTTP requests to test pages, APIs, or form submissions
|
||||||
- If you need to inspect the live site, use browser tools or HTTP requests to localhost:8080
|
- If you need to inspect the live site, use browser tools (Playwright MCP) or HTTP requests to localhost:8080
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
@@ -15,10 +15,12 @@ This is an Acai CMS website project. Follow these instructions when working with
|
|||||||
├── template/estandar/
|
├── template/estandar/
|
||||||
│ ├── modulos/ # Builder modules (visual components)
|
│ ├── modulos/ # Builder modules (visual components)
|
||||||
│ │ └── <module-id>/
|
│ │ └── <module-id>/
|
||||||
│ │ ├── index-base.tpl # Twig template (source)
|
│ │ ├── index-base.tpl # Twig template (source — EDIT THIS)
|
||||||
│ │ ├── index.tpl # Compiled template (auto-generated, do NOT edit)
|
|
||||||
│ │ ├── style.css # Module styles
|
│ │ ├── style.css # Module styles
|
||||||
│ │ └── script.js # Module JavaScript
|
│ │ └── script.js # Module JavaScript
|
||||||
|
│ │ ├── index.tpl # Compiled (auto-generated, do NOT edit)
|
||||||
|
│ │ ├── index-twig.tpl # Compiled (auto-generated, do NOT edit)
|
||||||
|
│ │ └── builder.json # Compiled builder vars (auto-generated, do NOT edit)
|
||||||
│ ├── css/ # Global CSS
|
│ ├── css/ # Global CSS
|
||||||
│ └── js/ # Global JavaScript
|
│ └── js/ # Global JavaScript
|
||||||
├── hooks/ # PHP hooks (server-side logic)
|
├── hooks/ # PHP hooks (server-side logic)
|
||||||
@@ -38,17 +40,28 @@ This is an Acai CMS website project. Follow these instructions when working with
|
|||||||
## Key Concepts
|
## Key Concepts
|
||||||
|
|
||||||
### Modules (`template/estandar/modulos/`)
|
### Modules (`template/estandar/modulos/`)
|
||||||
Visual components that the site builder uses. Each module is a self-contained unit with its own template (Twig), CSS, and JS. Modules are placed on pages via the drag-and-drop builder.
|
Visual components that the site builder uses. Each module is a self-contained unit with its own template (Twig + Acai attributes), CSS, and JS. Modules are placed on pages via the drag-and-drop builder. The editable file is always `index-base.tpl`.
|
||||||
|
|
||||||
|
- Include other modules: `<module_id :param1="value1"></module_id>`
|
||||||
|
- Each module instance gets a unique `section_id` variable for anchors/scoping
|
||||||
|
- Use `interno` variable to detect CMS editor mode vs public view
|
||||||
|
|
||||||
See [docs/modular-system.md](docs/modular-system.md) for detailed rules.
|
See [docs/modular-system.md](docs/modular-system.md) for detailed rules.
|
||||||
|
|
||||||
### General Sections
|
### General Sections
|
||||||
Reusable layout blocks (header, footer, sidebars) that appear across multiple pages. They use the same Twig engine as modules but serve a structural purpose.
|
Database-backed templates (headers, footers, record views) that use the `thisrecord` variable to access record fields. They use the same Twig + Acai attribute engine as modules.
|
||||||
|
|
||||||
|
- Upload fields return arrays: `thisrecord.image[0].urlPath`
|
||||||
|
- Foreign keys use `_num` suffix: `category_num`
|
||||||
|
|
||||||
See [docs/modular-system.md](docs/modular-system.md) for details.
|
See [docs/modular-system.md](docs/modular-system.md) for details.
|
||||||
|
|
||||||
### Hooks (`hooks/`)
|
### Hooks (`hooks/`)
|
||||||
PHP files that execute server-side logic at specific points: before/after page load, on form submission, on API calls, scheduled tasks, etc. Hooks extend the CMS behavior without modifying the core.
|
PHP files that execute server-side logic. Triggered by:
|
||||||
|
- Twig filter: `'hooks/module_id/' | hook({param: value})`
|
||||||
|
- HTML tag: `<hook result="var" endpoint="/hooks/module_id/" :param="value"></hook>`
|
||||||
|
- JavaScript: `CmsApi.hook('/hooks/module_id/', {param: value}, callback)`
|
||||||
|
- Form action: via `c-form` attribute
|
||||||
|
|
||||||
See [docs/hooks-and-api.md](docs/hooks-and-api.md) for usage.
|
See [docs/hooks-and-api.md](docs/hooks-and-api.md) for usage.
|
||||||
|
|
||||||
@@ -57,25 +70,41 @@ See [docs/hooks-and-api.md](docs/hooks-and-api.md) for usage.
|
|||||||
When the site is running in Docker, you can connect to the database:
|
When the site is running in Docker, you can connect to the database:
|
||||||
|
|
||||||
- **Host:** `127.0.0.1`
|
- **Host:** `127.0.0.1`
|
||||||
- **Port:** Check `.docker/.env` or `docker-compose.yml` for the mapped port (usually 3307+)
|
- **Port:** Check `.docker/docker-compose.yml` for the mapped port (usually 3307+)
|
||||||
- **Credentials:** Read from `.docker/.env`:
|
- **Credentials:** Read from `.docker/.env`:
|
||||||
- `DB_USERNAME`
|
- `DB_USERNAME`
|
||||||
- `DB_PASSWORD`
|
- `DB_PASSWORD`
|
||||||
- `DB_DATABASE`
|
- `DB_DATABASE`
|
||||||
|
|
||||||
You can also exec into the container:
|
|
||||||
```bash
|
```bash
|
||||||
docker exec -it dw-<project-name>-db mysql -u root -p<password> <database>
|
docker exec -it dw-<project-name>-db mysql -u root -p<password> <database>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Important:** Table names in CmsApi/Twig do NOT use the `cms_` prefix. The primary key is always `num`, never `id`.
|
||||||
|
|
||||||
## Acai Core (web-base)
|
## Acai Core (web-base)
|
||||||
|
|
||||||
The project workspace contains only the **customization layer** (modules, hooks, schemas, uploads). The CMS core (routing, rendering engine, admin panel, APIs) lives in a separate directory called **web-base** that is mounted as a Docker volume.
|
The project workspace contains only the **customization layer** (modules, hooks, schemas, uploads). The CMS core (routing, rendering engine, admin panel, APIs) lives in a separate directory called **web-base** that is mounted as a Docker volume.
|
||||||
|
|
||||||
If you need to understand core behavior (Twig filters, CmsApi methods, routing, etc.), the web-base path can be obtained from the plugin settings. Do NOT modify web-base files — they are shared across all projects.
|
The web-base path can be obtained via: `GET http://localhost:9090/api/web-base-path`
|
||||||
|
|
||||||
|
Do NOT modify web-base files — they are shared across all projects.
|
||||||
|
|
||||||
|
## Critical Rules
|
||||||
|
|
||||||
|
1. Only edit `index-base.tpl` in modules — `index.tpl`, `index-twig.tpl`, and `builder.json` are auto-generated
|
||||||
|
2. Use Twig **filters** (with `|`), never Twig functions
|
||||||
|
3. Table names without `cms_` prefix everywhere
|
||||||
|
4. Primary key is `num`, never `id`
|
||||||
|
5. Upload fields are arrays — access with `[0].urlPath`
|
||||||
|
6. Tailwind CSS as primary styling, custom CSS scoped with BEM when needed
|
||||||
|
7. Twig concatenation uses `~` operator: `'value=' ~ variable`
|
||||||
|
8. `enlace` (link) fields already include slashes
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
- [docs/modular-system.md](docs/modular-system.md) — Modules, general sections, Twig syntax, builder vars, custom filters
|
- [docs/modular-system.md](docs/modular-system.md) — Modules, general sections, global variables
|
||||||
- [docs/hooks-and-api.md](docs/hooks-and-api.md) — PHP hooks, CmsApi, CocoDB, database operations
|
|
||||||
- [docs/builder-fields.md](docs/builder-fields.md) — Builder field types, c-form, c-if/c-for/c-class, data-field-type
|
- [docs/builder-fields.md](docs/builder-fields.md) — Builder field types, c-form, c-if/c-for/c-class, data-field-type
|
||||||
|
- [docs/twig-filters.md](docs/twig-filters.md) — Twig filters reference (get, hook, module, queryDB, etc.)
|
||||||
|
- [docs/hooks-and-api.md](docs/hooks-and-api.md) — PHP hooks, CmsApi, CocoDB, database operations
|
||||||
|
- [docs/css-js-conventions.md](docs/css-js-conventions.md) — CSS/JS patterns, Tailwind, BEM, Vue 3 integration
|
||||||
|
|||||||
@@ -1,32 +1,213 @@
|
|||||||
# Builder Fields & Attributes
|
# Builder Fields & Acai Attributes
|
||||||
|
|
||||||
## Field Types (`data-field-type`)
|
## Field Types (`data-field-type`)
|
||||||
|
|
||||||
The builder uses `data-field-type` attributes to define editable areas in module templates.
|
The builder uses `data-field-type` attributes on HTML elements to define editable areas.
|
||||||
|
|
||||||
<!-- TODO: Document available field types (text, image, richtext, link, repeater, etc.) -->
|
| Type | Description | Returns |
|
||||||
|
|------|-------------|---------|
|
||||||
|
| `textfield` | Single line text | String |
|
||||||
|
| `headfield` | Heading text (generates `_tag` variable for semantic tag: h1-h6) | String |
|
||||||
|
| `textbox` | Multi-line text | String |
|
||||||
|
| `wysiwyg` | Rich text editor (HTML output) | HTML string |
|
||||||
|
| `link` | URL field (already includes slashes) | String |
|
||||||
|
| `upload` | Single image/file | Array: `[0].urlPath`, `[0].info1` (alt), `[0].info2-4` |
|
||||||
|
| `uploadMulti` | Multiple images | Iterable: `item.urlPath` |
|
||||||
|
| `list` | Dropdown (fixed options or from table) | String or foreign key num |
|
||||||
|
| `multiv2` | Repeatable group of fields | Array of objects |
|
||||||
|
|
||||||
|
### headfield Example
|
||||||
|
|
||||||
|
```html
|
||||||
|
<{{ title_tag | default('h2') }} data-field-type="headfield" class="text-3xl font-bold">
|
||||||
|
Section Title
|
||||||
|
</{{ title_tag | default('h2') }}>
|
||||||
|
```
|
||||||
|
|
||||||
|
### upload Example
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Single image -->
|
||||||
|
<img data-field-type="upload"
|
||||||
|
src="{{ image[0].urlPath }}"
|
||||||
|
alt="{{ image[0].info1 }}"
|
||||||
|
class="w-full rounded" />
|
||||||
|
|
||||||
|
<!-- Multiple images -->
|
||||||
|
<div c-for="photo in gallery">
|
||||||
|
<img src="{{ photo.urlPath }}" alt="{{ photo.info1 }}" />
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### list Example
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Fixed options -->
|
||||||
|
<select data-field-type="list" data-list-options="option1,option2,option3">
|
||||||
|
<option>option1</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- From table -->
|
||||||
|
<select data-field-type="list" data-list-table="categories" data-list-field="name">
|
||||||
|
<option>Category</option>
|
||||||
|
</select>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Conditional Attributes
|
## Acai Attributes
|
||||||
|
|
||||||
### `c-if`
|
### `c-if` — Conditional Rendering
|
||||||
|
|
||||||
<!-- TODO: Document c-if syntax and usage for conditional rendering -->
|
```html
|
||||||
|
<!-- Boolean check -->
|
||||||
|
<div c-if="showBanner">Banner content</div>
|
||||||
|
|
||||||
### `c-for`
|
<!-- Equality check (uses = not ==) -->
|
||||||
|
<div c-if="layout = 'grid'">Grid layout</div>
|
||||||
|
|
||||||
<!-- TODO: Document c-for syntax for repeating elements -->
|
<!-- Variable exists / is not empty -->
|
||||||
|
<div c-if="subtitle">{{ subtitle }}</div>
|
||||||
|
```
|
||||||
|
|
||||||
### `c-class`
|
### `c-else`
|
||||||
|
|
||||||
<!-- TODO: Document c-class for conditional CSS classes -->
|
Must immediately follow the `c-if` element:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div c-if="image">
|
||||||
|
<img src="{{ image[0].urlPath }}" />
|
||||||
|
</div>
|
||||||
|
<div c-else>
|
||||||
|
<p>No image available</p>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### `c-for` — Iteration
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Over array/multiv2 -->
|
||||||
|
<div c-for="item in record.features">
|
||||||
|
<h3>{{ item.title }}</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Over database table -->
|
||||||
|
<div c-for="product in products" c-where="active = 1" c-order="orden ASC" c-limit="6">
|
||||||
|
<h3>{{ product.nombre }}</h3>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Available inside loop: `loop.index` (1-based), `loop.index is odd`, `loop.index is even`
|
||||||
|
|
||||||
|
### `c-class` — Dynamic CSS Classes
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div c-class="{ 'bg-blue-500': isActive, 'text-white': isActive, 'hidden': !showElement }">
|
||||||
|
Content
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### `c-hidden` — Hidden Elements
|
||||||
|
|
||||||
|
Element is not rendered but can declare builder variables:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div c-hidden="true">
|
||||||
|
<input data-field-type="textfield" value="default config value" />
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### `c-required` — Conditional Required Fields
|
||||||
|
|
||||||
|
```html
|
||||||
|
<input type="text" name="company" c-required="userType = 'business'" />
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Forms (`c-form`)
|
## Forms (`c-form`)
|
||||||
|
|
||||||
<!-- TODO: Document c-form usage for builder-integrated forms -->
|
Complete form handling with automatic validation, storage, and email sending.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<form c-form
|
||||||
|
tableName="'contacto'"
|
||||||
|
mailRecord="['correos', 'CONTACTO']"
|
||||||
|
sendTo="'admin@domain.com'"
|
||||||
|
sendToClient="'email'"
|
||||||
|
captcha="true"
|
||||||
|
honeypot="true"
|
||||||
|
messageOK="'Mensaje enviado correctamente'"
|
||||||
|
messageKO="'Error al enviar el mensaje'"
|
||||||
|
redirect="'/gracias/'"
|
||||||
|
attachFiles="true"
|
||||||
|
showImages="true"
|
||||||
|
>
|
||||||
|
<input type="text" name="nombre" required placeholder="Nombre" />
|
||||||
|
<input type="email" name="email" required placeholder="Email" />
|
||||||
|
<textarea name="mensaje" required placeholder="Mensaje"></textarea>
|
||||||
|
<captcha/>
|
||||||
|
<button type="submit">Enviar</button>
|
||||||
|
</form>
|
||||||
|
```
|
||||||
|
|
||||||
|
### c-form Attributes
|
||||||
|
|
||||||
|
| Attribute | Description |
|
||||||
|
|-----------|-------------|
|
||||||
|
| `tableName="'table'"` | Store submissions in database table |
|
||||||
|
| `mailRecord="['correos', 'ID']"` | Email template from `correos` table |
|
||||||
|
| `sendTo="'email@domain.com'"` | Recipient email(s), comma-separated |
|
||||||
|
| `sendToClient="'fieldname'"` | Field containing client's email for auto-reply |
|
||||||
|
| `captcha="true"` | Enable Google reCAPTCHA |
|
||||||
|
| `honeypot="true"` | Anti-spam hidden field |
|
||||||
|
| `messageOK="'text'"` | Success message |
|
||||||
|
| `messageKO="'text'"` | Error message |
|
||||||
|
| `redirect="'/path/'"` | Redirect after successful submit |
|
||||||
|
| `attachFiles="true"` | Attach uploaded files to email |
|
||||||
|
| `showImages="true"` | Show image thumbnails in email |
|
||||||
|
|
||||||
|
|
||||||
## Builder Variables Access
|
## Built-in Components
|
||||||
|
|
||||||
<!-- TODO: Document how to access and use builder variables in templates -->
|
### Carousel (`c-tns-wrapper`)
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="c-tns-wrapper"
|
||||||
|
data-responsive='{"0":1,"768":2,"1024":3}'
|
||||||
|
data-speed="400"
|
||||||
|
data-nav="true"
|
||||||
|
data-autoplay-timeout="3000">
|
||||||
|
<div c-for="slide in record.slides">
|
||||||
|
<img src="{{ slide.image[0].urlPath }}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lightbox
|
||||||
|
|
||||||
|
```html
|
||||||
|
<a href="{{ image[0].urlPath }}" class="glightbox" data-gallery="gallery1">
|
||||||
|
<img src="{{ image[0].urlPath | imagec(400) }}" />
|
||||||
|
</a>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Breadcrumb
|
||||||
|
|
||||||
|
```html
|
||||||
|
<breadCrumb/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Animate On Scroll (AOS)
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div data-aos="fade-up" data-aos-delay="200">
|
||||||
|
Animated content
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lazy Loading
|
||||||
|
|
||||||
|
```html
|
||||||
|
<img class="lazyload" data-src="{{ image[0].urlPath }}" />
|
||||||
|
<!-- or -->
|
||||||
|
<img data-lazy="true" src="{{ image[0].urlPath }}" />
|
||||||
|
```
|
||||||
|
|||||||
101
docs/css-js-conventions.md
Normal file
101
docs/css-js-conventions.md
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
# CSS & JavaScript Conventions
|
||||||
|
|
||||||
|
## CSS
|
||||||
|
|
||||||
|
### Tailwind First
|
||||||
|
|
||||||
|
Use Tailwind CSS as the primary styling method. Only use custom CSS when Tailwind is insufficient.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Good: Tailwind classes -->
|
||||||
|
<div class="flex items-center gap-4 p-6 bg-white rounded-lg shadow-md">
|
||||||
|
<h2 class="text-2xl font-bold text-gray-900">Title</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Custom CSS only when needed (animations, complex selectors, etc.) -->
|
||||||
|
```
|
||||||
|
|
||||||
|
### BEM for Custom CSS
|
||||||
|
|
||||||
|
When custom CSS is needed, scope everything under a root class using BEM naming:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Root class in kebab-case */
|
||||||
|
.hero-section { }
|
||||||
|
.hero-section__title { }
|
||||||
|
.hero-section__image { }
|
||||||
|
.hero-section--dark { }
|
||||||
|
```
|
||||||
|
|
||||||
|
Never use global classes without a module prefix.
|
||||||
|
|
||||||
|
### CSS Variables
|
||||||
|
|
||||||
|
Acai provides theme variables:
|
||||||
|
|
||||||
|
```css
|
||||||
|
var(--main-color) /* Primary brand color */
|
||||||
|
var(--main-color-light) /* Lighter variant */
|
||||||
|
var(--main-color-dark) /* Darker variant */
|
||||||
|
```
|
||||||
|
|
||||||
|
### Utility Classes (Built-in)
|
||||||
|
|
||||||
|
| Class | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `transition3s` | 0.3s smooth transition |
|
||||||
|
| `click-a-child` | Makes parent clickable via child `<a>` tag |
|
||||||
|
| `line-clamp2` / `line-clamp3` / `line-clamp5` | Text truncation with ellipsis |
|
||||||
|
| `filter-white` | CSS filter to make images/icons white |
|
||||||
|
| `lazyload` | Lazy loading (use with `data-src`) |
|
||||||
|
|
||||||
|
|
||||||
|
## JavaScript
|
||||||
|
|
||||||
|
### Module Scripts (`script.js`)
|
||||||
|
|
||||||
|
Keep JavaScript scoped to the module. Use `section_id` for targeting:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Scope to this module instance
|
||||||
|
const section = document.getElementById('{{ section_id }}');
|
||||||
|
if (section) {
|
||||||
|
const buttons = section.querySelectorAll('.btn');
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### CmsApi (Client-Side)
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Call a hook
|
||||||
|
CmsApi.hook('/hooks/module_id/', { action: 'getData', id: 123 }, function(response) {
|
||||||
|
console.log(response);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vue 3 Integration
|
||||||
|
|
||||||
|
For complex interactivity, use Vue 3 via CDN with Composition API:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div id="app-{{ section_id }}">
|
||||||
|
<p>${ message }</p>
|
||||||
|
<button @click="increment">${ count }</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const { createApp, ref } = Vue;
|
||||||
|
createApp({
|
||||||
|
delimiters: ['${', '}'], // Avoid conflict with Twig {{ }}
|
||||||
|
setup() {
|
||||||
|
const message = ref('Hello');
|
||||||
|
const count = ref(0);
|
||||||
|
const increment = () => count.value++;
|
||||||
|
return { message, count, increment };
|
||||||
|
}
|
||||||
|
}).mount('#app-{{ section_id }}');
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important:** Use `'${'` and `'}'` as Vue delimiters to avoid conflicts with Twig's `{{ }}` syntax.
|
||||||
@@ -2,43 +2,141 @@
|
|||||||
|
|
||||||
## Hooks
|
## Hooks
|
||||||
|
|
||||||
Hooks are PHP files in the `hooks/` directory that execute server-side logic at specific lifecycle points.
|
Hooks are PHP files in the `hooks/` directory that execute server-side logic. They can also live inside a module at `template/estandar/modulos/<module-id>/hook.php`.
|
||||||
|
|
||||||
### Hook Types
|
### How to Call Hooks
|
||||||
|
|
||||||
<!-- TODO: Document hook types (before_page, after_page, on_form, on_api, cron, etc.) -->
|
**From Twig:**
|
||||||
|
```twig
|
||||||
|
{{ 'hooks/module_id/' | hook({param1: 'value1', param2: variable}) }}
|
||||||
|
```
|
||||||
|
|
||||||
### Hook File Naming
|
**From HTML (with result):**
|
||||||
|
```html
|
||||||
|
<hook result="myVar" endpoint="/hooks/module_id/" :param1="value1" :param2="'string'"></hook>
|
||||||
|
<p>{{ myVar }}</p>
|
||||||
|
```
|
||||||
|
|
||||||
<!-- TODO: Document naming conventions and how the CMS discovers hooks -->
|
**From JavaScript:**
|
||||||
|
```js
|
||||||
|
CmsApi.hook('/hooks/module_id/', { param1: 'value1' }, function(response) {
|
||||||
|
console.log(response);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
### Hook Examples
|
**From c-form:**
|
||||||
|
Hooks are automatically triggered on form submission when configured.
|
||||||
|
|
||||||
<!-- TODO: Add practical examples of common hooks -->
|
### Hook Parameters
|
||||||
|
|
||||||
|
Parameters are received as PHP variables:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
// Called with: 'hooks/my_hook/' | hook({category: 'electronics', limit: 10})
|
||||||
|
// Available as:
|
||||||
|
$category; // 'electronics'
|
||||||
|
$limit; // 10
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hook Return Values
|
||||||
|
|
||||||
|
Hooks can `echo` or `return` values. When called from Twig or `<hook>` tag, the output is captured into the result variable.
|
||||||
|
|
||||||
|
|
||||||
## CmsApi
|
## CmsApi (PHP)
|
||||||
|
|
||||||
The `CmsApi` class provides methods to interact with the CMS from hooks.
|
Server-side API for database operations. Available in all hooks.
|
||||||
|
|
||||||
<!-- TODO: Document main CmsApi methods (getPage, getRecord, createRecord, updateRecord, etc.) -->
|
### Read Records
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Get all records
|
||||||
|
$products = CmsApi::get('productos');
|
||||||
|
|
||||||
|
// With WHERE condition
|
||||||
|
$active = CmsApi::get('productos', ['active' => 1]);
|
||||||
|
|
||||||
|
// With order and limit
|
||||||
|
$latest = CmsApi::get('noticias', [], 'fecha DESC', 5);
|
||||||
|
|
||||||
|
// With operators
|
||||||
|
$expensive = CmsApi::get('productos', ['precio' => ['>=' => 100]]);
|
||||||
|
$search = CmsApi::get('productos', ['nombre' => ['LIKE' => '%keyword%']]);
|
||||||
|
$inList = CmsApi::get('productos', ['categoria_num' => ['IN' => [1, 2, 3]]]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Insert Records
|
||||||
|
|
||||||
|
```php
|
||||||
|
$newRecord = CmsApi::insert('contacto', [
|
||||||
|
'nombre' => 'John',
|
||||||
|
'email' => 'john@example.com',
|
||||||
|
'mensaje' => 'Hello',
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update Records
|
||||||
|
|
||||||
|
```php
|
||||||
|
CmsApi::update('productos',
|
||||||
|
['precio' => 29.99, 'activo' => 1], // fields to update
|
||||||
|
['num' => 42] // where condition
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delete Records
|
||||||
|
|
||||||
|
```php
|
||||||
|
CmsApi::delete('productos', ['num' => 42]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Important Rules
|
||||||
|
|
||||||
|
- Table names **without** `cms_` prefix
|
||||||
|
- Primary key is always `num`, never `id`
|
||||||
|
- Upload fields are handled separately (not via insert/update)
|
||||||
|
- Operators: `=`, `!=`, `>`, `>=`, `<`, `<=`, `LIKE`, `IN`
|
||||||
|
|
||||||
|
|
||||||
|
## CmsApi (JavaScript — Client-Side)
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Hook call
|
||||||
|
CmsApi.hook('/hooks/module_id/', { param: 'value' }, function(response) {
|
||||||
|
// response is the hook output
|
||||||
|
});
|
||||||
|
|
||||||
|
// Record operations (if exposed via hooks)
|
||||||
|
CmsApi.get('tableName', { where: conditions }, function(records) {
|
||||||
|
// records array
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## CocoDB
|
## CocoDB
|
||||||
|
|
||||||
`CocoDB` is the database abstraction layer for Acai.
|
Low-level database abstraction layer. Use when CmsApi is too high-level.
|
||||||
|
|
||||||
<!-- TODO: Document CocoDB usage (query, insert, update, delete, transactions) -->
|
<!-- TODO: Document CocoDB methods (query, insert, update, delete, transactions) -->
|
||||||
|
|
||||||
|
|
||||||
## Database Operations
|
## Table Schemas
|
||||||
|
|
||||||
### Direct Queries
|
Table schemas are stored as JSON in `cms/data/schema/`. Each file defines:
|
||||||
|
- Field names and types
|
||||||
|
- Validation rules
|
||||||
|
- Relationships (foreign keys)
|
||||||
|
- Display configuration
|
||||||
|
|
||||||
<!-- TODO: Document how to run raw SQL queries from hooks -->
|
### Field Format Types
|
||||||
|
|
||||||
### Table Schemas
|
| Type | PHP Format | Notes |
|
||||||
|
|------|-----------|-------|
|
||||||
Table schemas are stored as JSON files in `cms/data/schema/`. Each file defines the fields, types, and configuration of a CMS table.
|
| Text | String | Plain text |
|
||||||
|
| Date/time | `YYYY-MM-DD HH:mm:ss` | MySQL datetime format |
|
||||||
<!-- TODO: Document schema format and how to create/modify tables -->
|
| Checkbox | `1` or `0` | Boolean as integer |
|
||||||
|
| WYSIWYG | HTML string | Rich text with Tailwind classes |
|
||||||
|
| List | String or num | Foreign key if linked to table |
|
||||||
|
| Multivalores | JSON string | Serialized array |
|
||||||
|
| Upload | — | Handled separately, never in insert/update |
|
||||||
|
|||||||
@@ -4,51 +4,102 @@
|
|||||||
|
|
||||||
Modules are the visual building blocks of Acai websites. Each module lives in `template/estandar/modulos/<module-id>/`.
|
Modules are the visual building blocks of Acai websites. Each module lives in `template/estandar/modulos/<module-id>/`.
|
||||||
|
|
||||||
### File structure
|
### File Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
<module-id>/
|
<module-id>/
|
||||||
├── index-base.tpl # Source Twig template (EDIT THIS)
|
├── index-base.tpl # Source template (EDIT THIS)
|
||||||
├── index.tpl # Compiled output (auto-generated, do NOT edit)
|
├── index.tpl # Compiled output (auto-generated, do NOT edit)
|
||||||
|
├── index-twig.tpl # Compiled Twig output (auto-generated, do NOT edit)
|
||||||
|
├── builder.json # Compiled builder vars (auto-generated, do NOT edit)
|
||||||
├── style.css # Module-scoped styles
|
├── style.css # Module-scoped styles
|
||||||
└── script.js # Module JavaScript
|
└── script.js # Module JavaScript
|
||||||
```
|
```
|
||||||
|
|
||||||
### Twig Templates
|
### Template Syntax
|
||||||
|
|
||||||
<!-- TODO: Document Twig syntax, variable access, builder vars -->
|
Templates use a hybrid of **Twig** and **Acai attributes**. The source file is always `index-base.tpl`.
|
||||||
|
|
||||||
### Custom Twig Filters
|
```html
|
||||||
|
<section class="hero-section" id="{{ section_id }}">
|
||||||
|
<div class="container mx-auto px-4">
|
||||||
|
<h2 data-field-type="headfield" class="text-3xl font-bold">
|
||||||
|
Title here
|
||||||
|
</h2>
|
||||||
|
<p data-field-type="textbox" class="text-lg text-gray-600">
|
||||||
|
Description text
|
||||||
|
</p>
|
||||||
|
<img data-field-type="upload" src="placeholder.jpg" class="w-full rounded-lg" />
|
||||||
|
<a data-field-type="link" href="#" class="btn">Call to action</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
```
|
||||||
|
|
||||||
<!-- TODO: Document custom filters available in Acai (e.g., |translate, |slugify, etc.) -->
|
### Including Modules from Other Modules
|
||||||
|
|
||||||
### Builder Variables
|
```html
|
||||||
|
<module_id :param1="value1" :param2="'string value'"></module_id>
|
||||||
|
```
|
||||||
|
|
||||||
<!-- TODO: Document how builder vars work, how to access them in templates -->
|
Parameters are received as variables inside the included module.
|
||||||
|
|
||||||
### Calling Modules from Other Modules
|
### Global Variables
|
||||||
|
|
||||||
<!-- TODO: Document how to include/call modules from other modules or general sections -->
|
| Variable | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `section_id` | Unique ID per module instance (use for anchors, JS scoping) |
|
||||||
|
| `interno` | `true` when viewing in CMS editor, `false` on public site |
|
||||||
|
| `server.HTTP_HOST` | Current domain |
|
||||||
|
| `loop.index` | 1-based iteration index (inside `c-for`) |
|
||||||
|
| `loop.index is odd` / `loop.index is even` | For alternating layouts |
|
||||||
|
|
||||||
|
|
||||||
## General Sections
|
## General Sections
|
||||||
|
|
||||||
General sections are reusable layout blocks (headers, footers, sidebars) shared across pages.
|
General sections are database-backed templates used for record views, headers, footers, and reusable layouts. They use the same template engine as modules.
|
||||||
|
|
||||||
<!-- TODO: Document how general sections differ from modules, where they live, how to create/edit them -->
|
### Key Differences from Modules
|
||||||
|
|
||||||
|
- Access record data via the `thisrecord` variable
|
||||||
|
- Upload fields return **arrays**: `thisrecord.image[0].urlPath`
|
||||||
|
- Additional upload metadata: `info1` (alt text), `info2`, `info3`, `info4`
|
||||||
|
- Foreign key fields use `_num` suffix: `thisrecord.category_num`
|
||||||
|
- Saved via `save_general_section()` (not `save_module()`)
|
||||||
|
- Parser type 2 = Twig (recommended), 0 = Acai legacy syntax
|
||||||
|
|
||||||
|
### Example: Record Template
|
||||||
|
|
||||||
|
```html
|
||||||
|
<article class="product-card">
|
||||||
|
<img src="{{ thisrecord.imagen[0].urlPath }}"
|
||||||
|
alt="{{ thisrecord.imagen[0].info1 }}"
|
||||||
|
class="w-full h-64 object-cover" />
|
||||||
|
<h3 class="text-xl font-semibold">{{ thisrecord.nombre }}</h3>
|
||||||
|
<p class="text-gray-600">{{ thisrecord.descripcion | raw }}</p>
|
||||||
|
<span class="text-2xl font-bold">{{ thisrecord.precio }}€</span>
|
||||||
|
</article>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Variable Assignment
|
||||||
|
|
||||||
|
Use `<set>` tag to create variables from queries:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<set :categories="'categorias' | get()"></set>
|
||||||
|
<set :featured="'productos' | get({destacado: 1}, 'orden ASC', 3)"></set>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## CSS & JavaScript
|
## Repeatable Content (multiv2)
|
||||||
|
|
||||||
### Module Styles (`style.css`)
|
The `multiv2` builder field type creates repeatable groups of fields:
|
||||||
|
|
||||||
<!-- TODO: Document scoping rules, CSS conventions -->
|
```html
|
||||||
|
<div c-for="item in record.items">
|
||||||
|
<h3 data-field-type="textfield">{{ item.title }}</h3>
|
||||||
|
<p data-field-type="textbox">{{ item.description }}</p>
|
||||||
|
<img data-field-type="upload" src="{{ item.image }}" />
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
### Module Scripts (`script.js`)
|
Access individual items: `record.items[0].title`, `record.items[1].image`, etc.
|
||||||
|
|
||||||
<!-- TODO: Document JS conventions, lifecycle, event handling -->
|
|
||||||
|
|
||||||
|
|
||||||
## Builder Field Types
|
|
||||||
|
|
||||||
<!-- TODO: Document data-field-type usage, c-form, c-if, c-for, c-class attributes -->
|
|
||||||
|
|||||||
118
docs/twig-filters.md
Normal file
118
docs/twig-filters.md
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
# Twig Filters Reference
|
||||||
|
|
||||||
|
Acai uses Twig **filters** (with `|` pipe syntax). Never use Twig functions — only filters are supported.
|
||||||
|
|
||||||
|
## Database Queries
|
||||||
|
|
||||||
|
### `get` — Query Table
|
||||||
|
|
||||||
|
```twig
|
||||||
|
{# All records #}
|
||||||
|
{% set products = 'productos' | get() %}
|
||||||
|
|
||||||
|
{# With WHERE #}
|
||||||
|
{% set active = 'productos' | get({activo: 1}) %}
|
||||||
|
|
||||||
|
{# With WHERE + ORDER + LIMIT #}
|
||||||
|
{% set latest = 'noticias' | get({publicado: 1}, 'fecha DESC', 6) %}
|
||||||
|
|
||||||
|
{# Single record (first result) #}
|
||||||
|
{% set product = 'productos' | get({num: 42}) %}
|
||||||
|
{{ product[0].nombre }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `queryDB` — Raw SQL Query
|
||||||
|
|
||||||
|
```twig
|
||||||
|
{% set results = 'SELECT * FROM cms_productos WHERE precio > 100 ORDER BY precio ASC' | queryDB() %}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: Raw SQL uses the full table name WITH `cms_` prefix.
|
||||||
|
|
||||||
|
## Module & Hook Execution
|
||||||
|
|
||||||
|
### `hook` — Execute PHP Hook
|
||||||
|
|
||||||
|
```twig
|
||||||
|
{# Call hook and output result #}
|
||||||
|
{{ 'hooks/module_id/' | hook({param1: 'value', param2: variable}) }}
|
||||||
|
|
||||||
|
{# Capture into variable #}
|
||||||
|
{% set result = 'hooks/module_id/' | hook({action: 'getData'}) %}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `module` — Render Another Module
|
||||||
|
|
||||||
|
```twig
|
||||||
|
{{ 'other_module_id' | module({param1: value1}) }}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Text & Content
|
||||||
|
|
||||||
|
### `translate` — Translation
|
||||||
|
|
||||||
|
```twig
|
||||||
|
{{ 'Hello' | translate }}
|
||||||
|
{{ variable | translate }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `raw` — Render HTML Without Escaping
|
||||||
|
|
||||||
|
```twig
|
||||||
|
{{ record.description | raw }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `truncate` — Text Truncation
|
||||||
|
|
||||||
|
```twig
|
||||||
|
{{ record.description | truncate(150) }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `json_decode` — Parse JSON String
|
||||||
|
|
||||||
|
```twig
|
||||||
|
{% set data = jsonString | json_decode %}
|
||||||
|
{{ data.key }}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Images
|
||||||
|
|
||||||
|
### `imagec` — Image Optimization/Resize
|
||||||
|
|
||||||
|
```twig
|
||||||
|
{# Resize to width #}
|
||||||
|
<img src="{{ record.image[0].urlPath | imagec(400) }}" />
|
||||||
|
|
||||||
|
{# In srcset #}
|
||||||
|
<img src="{{ record.image[0].urlPath | imagec(800) }}"
|
||||||
|
srcset="{{ record.image[0].urlPath | imagec(400) }} 400w,
|
||||||
|
{{ record.image[0].urlPath | imagec(800) }} 800w" />
|
||||||
|
```
|
||||||
|
|
||||||
|
## Operators & Syntax
|
||||||
|
|
||||||
|
### Concatenation
|
||||||
|
|
||||||
|
Twig uses `~` for string concatenation (not `.` or `+`):
|
||||||
|
|
||||||
|
```twig
|
||||||
|
{{ 'Hello ' ~ name ~ '!' }}
|
||||||
|
{% set url = '/products/' ~ product.slug ~ '/' %}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ternary / Default
|
||||||
|
|
||||||
|
```twig
|
||||||
|
{{ title | default('Default Title') }}
|
||||||
|
{{ isActive ? 'active' : 'inactive' }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Comparisons in Twig
|
||||||
|
|
||||||
|
```twig
|
||||||
|
{% if items | length > 0 %}
|
||||||
|
{% if type == 'premium' %}
|
||||||
|
{% if name is not empty %}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: In `c-if` attributes, use `=` (single equals) for equality. In Twig `{% if %}` blocks, use `==` (double equals).
|
||||||
Reference in New Issue
Block a user