There are two basic modules:
- front (aka front-office)
- admin (aka back-office)
- nodejs with npm
- Docker
- MySQL Workbench
- start: $
ddev start
- server:
db
- Uživatel:
db
- Heslo:
db
- connect to ddev container:
ddev ssh
- run
npm install
to install frontend dependencies - run
docker compose up -d
to start the PHP server - chmod 777 for
./temp
and./log
- run
npm start
ornpm start:admin
to start the ViteJS server - open http://localhost/ to see the resulting website (any file changes reloads the browser for fast local development)
- open http://localhost:8080/ for database administration
- if you need to run a composer command, run
docker run -it --rm -v $(pwd):/app composer <command>
run npm run build
if you want to create production build of the front module. For the admin module
use npm run build:admin
command.
Run npm run lint:css
and npm run lint:js
before you commit to check
if your code adheres to the coding standard. It automatically fixes
problems with formatting. Issues which cannot be solved automatically
are displayed to the console.
All commit messages must be written in English in present tense.
Formatting is handled by Prettier. Standard
is enforced by ESLint rules (see eslintConfig.rules
in package.json
for a reference).
Whenever possible use Tailwind utility classes. For custom CSS, formatting is handled
by Prettier and standard
is enforced by Stylelint rules (see stylelint.rules
in package.json
for a reference).
Stick to Clean Code concepts: https://github.com/jupeter/clean-code-php
Learn to use all HTML tags. MDN is your friend. <div>
is
the last resort.
Use ./dev
and its respective subfolders to create or edit front-end assets.
Here is an example of the folder structure:
/dev
|-- admin
| |-- (same structure as front)
`-- front
|-- images
| |-- photo.jpg
| `-- logo.svg
|-- icons
| |-- mail.svg
| `-- arrow.svg
|-- css
| |-- index.css
| `-- contact.css
|-- js
| |-- index.js
| `-- contact.js
All assets are compiled into /www/dist/<modlue>
folder.
Because of cache busting, the only way of using your assets is
by means of ViteAssets
service. It's a dependency of BasePresenter
and you can use it in your
templates.
Examples:
<head>
{$viteAssets->printFrontTags("js/index.js")}
</head>
Static images are generated into www/dist/<module>/images
folder:
<img src="{$basePath}/dist/front/images/logo.svg" alt="Logo">
For each image the build system attempts to create several file types (jpg, webp, png and avif). You can take advantage
of this and use them in <picture>
element.
<picture>
<source
type="image/avif"
src="{$basePath}/dist/front/images/pomodoro.avif"
/>
<source
type="image/webp"
src="{$basePath}/dist/front/images/pomodoro.webp"
/>
<img
src="{$basePath}/dist/front/images/pomodoro.jpg"
alt="Some great alternative text"
loading="lazy"
decoding="async"
width="2032"
height="1076"
/>
</picture>
Images from the CMS should be processed using the image
or srcset
Latte filter:
<img src="{$user->avatar|image:thumbnail}" alt="Logo">
The image
filter accepts one parameter which is the configuration key:
# config.neon
fileStorageOptions:
aliases:
thumbnail: # this is the key
resize: [150, 150, Nette\Utils\Image::EXACT]
shapren: null
crop: ['100%', '50%', '80%', '80%']
srcset: [640, 768, 1024, 1366, 1600, 1920]
Every alias is an object where the key is a method name for Nette\Utils\Image and the value is an array of arguments for the method.
The srcset
filter can be used for responsive images:
<img src="{$user->avatar|image}" srcset="{$user->avatar|srcset}" alt="Logo">
<!--becomes-->
<img src="avatar.jpg" srcset="avatar-640.jpg 640w, ..." alt="Logo">
This repo holds only the most basic entities which are common to most clients content management systems (
eg. Article
, Tag
and User
). Probably, you will need to adjust them to meet specification of your new project. If
so, here is the recommended workflow:
Open the database_source.mwb
with MySQL Workbench and change the tables
and its columns. When ready export the SQL script (File → Export → SQL Script) and save it to init-db.sql
.
Use model classes in app/model
and presenters in app/modules/Admin/presenters
as examples to integrate a new entity.
For developers convenience there is DynamicForm
class for entity management.
Include DynamicFormFactory with DI. Here is a basic example:
use App\Components\DynamicForm;
use Nette\Forms\Controls\TextArea;
use App\AdminModule\Factories\DynamicFormFactory;
class Presenter extends \Nette\Application\UI\Presenter {
public DynamicFormFactory $dynamicFormFactory;
public function __construct(
DynamicFormFactory $dynamicFormFactory
) {
parent::__construct();
$this->dynamicFormFactory = $dynamicFormFactory;
}
public function createComponentForm(): DynamicForm {
return $this->dynamicFormFactory->create(
// definition callback
function (DynamicForm $form) {
$form->addImageUpload("image", "Obrázek");
},
// submit handler
function (array $values, ?int $id) {
$this->model->upsert($values, $id);
},
// caption (string or array)
"položku",
// default values
$this->model->getData()
);
}
}
This callback describes what inputs should be rendered. Submit input is automatically inserted at the end of the form. It provides you the instance of DynamicForm which monkey-patches Nette/Form input methods to play nicely with UIKit frontend framework. It also provides some convenience methods listed below:
This method adds data-ajax
attribute to the form, and it enables ajax behaviour. See initNetteAjax()
in dev/admin/js/imports/initilaizers.js
.
Often times you need to manage sub-entities inside one form. For example an article may have many sections. This is a perfect case for the multiplier which provides you the instance of Nette\Forms\Container for sub-entity description. It is extended with methods listed below. Here is an example of the multiplier with argument description:
$form->addMultiplier(
'sections', // name
function(Container $container) {
$container->addText('slug', 'Slug');
$container->addTranslation(
'label',
fn($label) => new TextInput($label),
'Popisek'
);
$container->addMultiplier(
'icons',
function(Container $container) {
$container->addImageUpload('icon', 'Ikona');
$container->addText('caption', 'Název');
}
);
);
},
["Sekce", "sekci"], // labels [normative case, genitive case] (optional),
"article-sections", // html id (optional)
["data-text" => 42] // html attributes (optional)
);
This method sets attribute type
to color
.
See MDN.
This method renders CKEditor WYSIWYG component. In translation, use textAreaToWysiwyg public method.
This method renders input for all project's locales. They are then displayed and hidden by UIKit Tab component. Here is an example of the translation with argument description:
$form->addTranslation(
// name
'content',
// definition callback (this is how to render wysiwyg)
fn($caption) => $form->textAreaToWysiwyg(new TextArea($caption)),
// caption (it's rendered with locales name in the brackets)
'Obsah',
// optional className
'uk-width-1/2'
);
This method renders Toast UI ImageEditor.
This method renders Uppy.
This method groups inputs into a fieldset grid. You can specify caption, number of columns and alignment of the grid.
User's save action invokes this function. It provides you array $formData
, ?int $id
of the record (derived
from $defaults
) to easily distinguish between update and insert, and Form $form
for form manipulation.
This can be either a string or an array made of two strings. In the first case, the form generates form's title and
submit text by prepending Uložit / Upravit
keywords. If you provide an array, you have direct control on the title,
and the submit text. For example: ['Hello buddy', 'Sign up']
.
Provide an associative array with the same shape as you've defined in the $onRender
parameter to prefill the form. If
the $defaults
contains a key named id
, it will be passed as a second parameter to the $onSubmit
callback.
This optional parameter forces the form to render as compact as possible to save vertical space.