Add Project-Pro Blog module with initial SQL setup, CSS styles, and template files
- Created SQL installation scripts for categories and posts tables. - Added uninstall scripts to drop the created tables. - Introduced CSS styles for blog layout, including responsive design for posts and categories. - Implemented PHP redirection in index files to prevent direct access. - Developed Smarty templates for blog category tree, post list, and individual post details. - Ensured proper caching headers in PHP files to enhance performance.
This commit is contained in:
124
.htaccess
Normal file
124
.htaccess
Normal file
@@ -0,0 +1,124 @@
|
||||
SecRuleEngine Off
|
||||
SecRequestBodyAccess Off
|
||||
|
||||
AddHandler application/x-httpd-php72 php
|
||||
php_value error_reporting 32767
|
||||
php_value log_errors on
|
||||
php_value display_errors off
|
||||
|
||||
# ~~start~~ Do not remove this comment, Prestashop will keep automatically the code outside this comment when .htaccess will be generated again
|
||||
# .htaccess automaticaly generated by PrestaShop e-commerce open-source solution
|
||||
# https://www.prestashop.com - https://www.prestashop.com/forums
|
||||
|
||||
<IfModule mod_rewrite.c>
|
||||
<IfModule mod_env.c>
|
||||
SetEnv HTTP_MOD_REWRITE On
|
||||
</IfModule>
|
||||
|
||||
RewriteEngine on
|
||||
RewriteCond %{HTTP:Authorization} ^(.*)
|
||||
RewriteRule . - [E=HTTP_AUTHORIZATION:%1]
|
||||
|
||||
|
||||
|
||||
#Domain: www.kikiriki.sklep.pl
|
||||
RewriteRule . - [E=REWRITEBASE:/]
|
||||
RewriteRule ^api(?:/(.*))?$ %{ENV:REWRITEBASE}webservice/dispatcher.php?url=$1 [QSA,L]
|
||||
RewriteRule ^upload/.+$ %{ENV:REWRITEBASE}index.php [QSA,L]
|
||||
|
||||
# Images
|
||||
RewriteCond %{HTTP_HOST} ^www.kikiriki.sklep.pl$
|
||||
RewriteRule ^([0-9])(\-[_a-zA-Z0-9-]*)?(-[0-9]+)?/.+\.jpg$ %{ENV:REWRITEBASE}img/p/$1/$1$2$3.jpg [L]
|
||||
RewriteCond %{HTTP_HOST} ^www.kikiriki.sklep.pl$
|
||||
RewriteRule ^([0-9])([0-9])(\-[_a-zA-Z0-9-]*)?(-[0-9]+)?/.+\.jpg$ %{ENV:REWRITEBASE}img/p/$1/$2/$1$2$3$4.jpg [L]
|
||||
RewriteCond %{HTTP_HOST} ^www.kikiriki.sklep.pl$
|
||||
RewriteRule ^([0-9])([0-9])([0-9])(\-[_a-zA-Z0-9-]*)?(-[0-9]+)?/.+\.jpg$ %{ENV:REWRITEBASE}img/p/$1/$2/$3/$1$2$3$4$5.jpg [L]
|
||||
RewriteCond %{HTTP_HOST} ^www.kikiriki.sklep.pl$
|
||||
RewriteRule ^([0-9])([0-9])([0-9])([0-9])(\-[_a-zA-Z0-9-]*)?(-[0-9]+)?/.+\.jpg$ %{ENV:REWRITEBASE}img/p/$1/$2/$3/$4/$1$2$3$4$5$6.jpg [L]
|
||||
RewriteCond %{HTTP_HOST} ^www.kikiriki.sklep.pl$
|
||||
RewriteRule ^([0-9])([0-9])([0-9])([0-9])([0-9])(\-[_a-zA-Z0-9-]*)?(-[0-9]+)?/.+\.jpg$ %{ENV:REWRITEBASE}img/p/$1/$2/$3/$4/$5/$1$2$3$4$5$6$7.jpg [L]
|
||||
RewriteCond %{HTTP_HOST} ^www.kikiriki.sklep.pl$
|
||||
RewriteRule ^([0-9])([0-9])([0-9])([0-9])([0-9])([0-9])(\-[_a-zA-Z0-9-]*)?(-[0-9]+)?/.+\.jpg$ %{ENV:REWRITEBASE}img/p/$1/$2/$3/$4/$5/$6/$1$2$3$4$5$6$7$8.jpg [L]
|
||||
RewriteCond %{HTTP_HOST} ^www.kikiriki.sklep.pl$
|
||||
RewriteRule ^([0-9])([0-9])([0-9])([0-9])([0-9])([0-9])([0-9])(\-[_a-zA-Z0-9-]*)?(-[0-9]+)?/.+\.jpg$ %{ENV:REWRITEBASE}img/p/$1/$2/$3/$4/$5/$6/$7/$1$2$3$4$5$6$7$8$9.jpg [L]
|
||||
RewriteCond %{HTTP_HOST} ^www.kikiriki.sklep.pl$
|
||||
RewriteRule ^c/([0-9]+)(\-[\.*_a-zA-Z0-9-]*)(-[0-9]+)?/.+\.jpg$ %{ENV:REWRITEBASE}img/c/$1$2$3.jpg [L]
|
||||
RewriteCond %{HTTP_HOST} ^www.kikiriki.sklep.pl$
|
||||
RewriteRule ^c/([a-zA-Z_-]+)(-[0-9]+)?/.+\.jpg$ %{ENV:REWRITEBASE}img/c/$1$2.jpg [L]
|
||||
# AlphaImageLoader for IE and fancybox
|
||||
RewriteRule ^images_ie/?([^/]+)\.(jpe?g|png|gif)$ js/jquery/plugins/fancybox/images/$1.$2 [L]
|
||||
|
||||
# Dispatcher
|
||||
RewriteCond %{REQUEST_FILENAME} -s [OR]
|
||||
RewriteCond %{REQUEST_FILENAME} -l [OR]
|
||||
RewriteCond %{REQUEST_FILENAME} -d
|
||||
RewriteRule ^.*$ - [NC,L]
|
||||
RewriteRule ^.*$ %{ENV:REWRITEBASE}index.php [NC,L]
|
||||
</IfModule>
|
||||
|
||||
AddType application/vnd.ms-fontobject .eot
|
||||
AddType font/ttf .ttf
|
||||
AddType font/otf .otf
|
||||
AddType application/font-woff .woff
|
||||
AddType font/woff2 .woff2
|
||||
<IfModule mod_headers.c>
|
||||
<FilesMatch "\.(ttf|ttc|otf|eot|woff|woff2|svg)$">
|
||||
Header set Access-Control-Allow-Origin "*"
|
||||
</FilesMatch>
|
||||
|
||||
<FilesMatch "\.pdf$">
|
||||
Header set Content-Disposition "Attachment"
|
||||
Header set X-Content-Type-Options "nosniff"
|
||||
</FilesMatch>
|
||||
</IfModule>
|
||||
|
||||
<Files composer.lock>
|
||||
# Apache 2.2
|
||||
<IfModule !mod_authz_core.c>
|
||||
Order deny,allow
|
||||
Deny from all
|
||||
</IfModule>
|
||||
|
||||
# Apache 2.4
|
||||
<IfModule mod_authz_core.c>
|
||||
Require all denied
|
||||
</IfModule>
|
||||
</Files>
|
||||
<IfModule mod_expires.c>
|
||||
ExpiresActive On
|
||||
ExpiresByType image/gif "access plus 1 month"
|
||||
ExpiresByType image/jpeg "access plus 1 month"
|
||||
ExpiresByType image/png "access plus 1 month"
|
||||
ExpiresByType text/css "access plus 1 week"
|
||||
ExpiresByType text/javascript "access plus 1 week"
|
||||
ExpiresByType application/javascript "access plus 1 week"
|
||||
ExpiresByType application/x-javascript "access plus 1 week"
|
||||
ExpiresByType image/x-icon "access plus 1 year"
|
||||
ExpiresByType image/svg+xml "access plus 1 year"
|
||||
ExpiresByType image/vnd.microsoft.icon "access plus 1 year"
|
||||
ExpiresByType application/font-woff "access plus 1 year"
|
||||
ExpiresByType application/x-font-woff "access plus 1 year"
|
||||
ExpiresByType font/woff2 "access plus 1 year"
|
||||
ExpiresByType application/vnd.ms-fontobject "access plus 1 year"
|
||||
ExpiresByType font/opentype "access plus 1 year"
|
||||
ExpiresByType font/ttf "access plus 1 year"
|
||||
ExpiresByType font/otf "access plus 1 year"
|
||||
ExpiresByType application/x-font-ttf "access plus 1 year"
|
||||
ExpiresByType application/x-font-otf "access plus 1 year"
|
||||
</IfModule>
|
||||
|
||||
<IfModule mod_headers.c>
|
||||
Header unset Etag
|
||||
</IfModule>
|
||||
FileETag none
|
||||
<IfModule mod_deflate.c>
|
||||
<IfModule mod_filter.c>
|
||||
AddOutputFilterByType DEFLATE text/html text/css text/javascript application/javascript application/x-javascript font/ttf application/x-font-ttf font/otf application/x-font-otf font/opentype image/svg+xml
|
||||
</IfModule>
|
||||
</IfModule>
|
||||
|
||||
#If rewrite mod isn't enabled
|
||||
ErrorDocument 404 /index.php?controller=404
|
||||
|
||||
# ~~end~~ Do not remove this comment, Prestashop will keep automatically the code outside this comment when .htaccess will be generated again
|
||||
|
||||
17
.mcp.json
Normal file
17
.mcp.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"serena": {
|
||||
"command": "C:\\Users\\jacek\\AppData\\Local\\Microsoft\\WinGet\\Packages\\astral-sh.uv_Microsoft.Winget.Source_8wekyb3d8bbwe\\uvx.exe",
|
||||
"args": [
|
||||
"--from",
|
||||
"git+https://github.com/oraios/serena",
|
||||
"serena",
|
||||
"start-mcp-server",
|
||||
"--context",
|
||||
"ide-assistant",
|
||||
"--project",
|
||||
"C:\\visual studio code\\projekty\\kikiriki.sklep.pl"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
1
.serena/.gitignore
vendored
Normal file
1
.serena/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/cache
|
||||
126
.serena/project.yml
Normal file
126
.serena/project.yml
Normal file
@@ -0,0 +1,126 @@
|
||||
# the name by which the project can be referenced within Serena
|
||||
project_name: "kikiriki.sklep.pl"
|
||||
|
||||
|
||||
# list of languages for which language servers are started; choose from:
|
||||
# al bash clojure cpp csharp
|
||||
# csharp_omnisharp dart elixir elm erlang
|
||||
# fortran fsharp go groovy haskell
|
||||
# java julia kotlin lua markdown
|
||||
# matlab nix pascal perl php
|
||||
# php_phpactor powershell python python_jedi r
|
||||
# rego ruby ruby_solargraph rust scala
|
||||
# swift terraform toml typescript typescript_vts
|
||||
# vue yaml zig
|
||||
# (This list may be outdated. For the current list, see values of Language enum here:
|
||||
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py
|
||||
# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
|
||||
# Note:
|
||||
# - For C, use cpp
|
||||
# - For JavaScript, use typescript
|
||||
# - For Free Pascal/Lazarus, use pascal
|
||||
# Special requirements:
|
||||
# Some languages require additional setup/installations.
|
||||
# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers
|
||||
# When using multiple languages, the first language server that supports a given file will be used for that file.
|
||||
# The first language is the default language and the respective language server will be used as a fallback.
|
||||
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
|
||||
languages:
|
||||
- php
|
||||
|
||||
# the encoding used by text files in the project
|
||||
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
|
||||
encoding: "utf-8"
|
||||
|
||||
# The language backend to use for this project.
|
||||
# If not set, the global setting from serena_config.yml is used.
|
||||
# Valid values: LSP, JetBrains
|
||||
# Note: the backend is fixed at startup. If a project with a different backend
|
||||
# is activated post-init, an error will be returned.
|
||||
language_backend:
|
||||
|
||||
# whether to use project's .gitignore files to ignore files
|
||||
ignore_all_files_in_gitignore: true
|
||||
|
||||
# list of additional paths to ignore in this project.
|
||||
# Same syntax as gitignore, so you can use * and **.
|
||||
# Note: global ignored_paths from serena_config.yml are also applied additively.
|
||||
ignored_paths: []
|
||||
|
||||
# whether the project is in read-only mode
|
||||
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
|
||||
# Added on 2025-04-18
|
||||
read_only: false
|
||||
|
||||
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
|
||||
# Below is the complete list of tools for convenience.
|
||||
# To make sure you have the latest list of tools, and to view their descriptions,
|
||||
# execute `uv run scripts/print_tool_overview.py`.
|
||||
#
|
||||
# * `activate_project`: Activates a project by name.
|
||||
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
|
||||
# * `create_text_file`: Creates/overwrites a file in the project directory.
|
||||
# * `delete_lines`: Deletes a range of lines within a file.
|
||||
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
|
||||
# * `execute_shell_command`: Executes a shell command.
|
||||
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
|
||||
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
|
||||
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
|
||||
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
||||
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
|
||||
# * `initial_instructions`: Gets the initial instructions for the current project.
|
||||
# Should only be used in settings where the system prompt cannot be set,
|
||||
# e.g. in clients you have no control over, like Claude Desktop.
|
||||
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
|
||||
# * `insert_at_line`: Inserts content at a given line in a file.
|
||||
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
|
||||
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
|
||||
# * `list_memories`: Lists memories in Serena's project-specific memory store.
|
||||
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
|
||||
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
|
||||
# * `read_file`: Reads a file within the project directory.
|
||||
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
|
||||
# * `remove_project`: Removes a project from the Serena configuration.
|
||||
# * `replace_lines`: Replaces a range of lines within a file with new content.
|
||||
# * `replace_symbol_body`: Replaces the full definition of a symbol.
|
||||
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
|
||||
# * `search_for_pattern`: Performs a search for a pattern in the project.
|
||||
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
|
||||
# * `switch_modes`: Activates modes by providing a list of their names
|
||||
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
|
||||
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
|
||||
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
|
||||
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
|
||||
excluded_tools: []
|
||||
|
||||
# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default)
|
||||
included_optional_tools: []
|
||||
|
||||
# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
|
||||
# This cannot be combined with non-empty excluded_tools or included_optional_tools.
|
||||
fixed_tools: []
|
||||
|
||||
# list of mode names to that are always to be included in the set of active modes
|
||||
# The full set of modes to be activated is base_modes + default_modes.
|
||||
# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply.
|
||||
# Otherwise, this setting overrides the global configuration.
|
||||
# Set this to [] to disable base modes for this project.
|
||||
# Set this to a list of mode names to always include the respective modes for this project.
|
||||
base_modes:
|
||||
|
||||
# list of mode names that are to be activated by default.
|
||||
# The full set of modes to be activated is base_modes + default_modes.
|
||||
# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply.
|
||||
# Otherwise, this overrides the setting from the global configuration (serena_config.yml).
|
||||
# This setting can, in turn, be overridden by CLI parameters (--mode).
|
||||
default_modes:
|
||||
|
||||
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
||||
# (contrary to the memories, which are loaded on demand).
|
||||
initial_prompt: ""
|
||||
|
||||
# time budget (seconds) per tool call for the retrieval of additional symbol information
|
||||
# such as docstrings or parameter information.
|
||||
# This overrides the corresponding setting in the global configuration; see the documentation there.
|
||||
# If null or missing, use the setting from the global configuration.
|
||||
symbol_info_budget:
|
||||
5
.vscode/ftp-kr.json
vendored
5
.vscode/ftp-kr.json
vendored
@@ -12,6 +12,9 @@
|
||||
"ignoreRemoteModification": true,
|
||||
"ignore": [
|
||||
".git",
|
||||
"/.vscode"
|
||||
"/.vscode",
|
||||
"/.claude",
|
||||
"/.serena",
|
||||
"CLAUDE.md"
|
||||
]
|
||||
}
|
||||
69
CLAUDE.md
Normal file
69
CLAUDE.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is a **PrestaShop 1.7.8.11** e-commerce store (`kikiriki.sklep.pl`). The repository contains only the customized parts of the installation — overrides, custom modules, theme templates, and configuration — not the full PrestaShop core.
|
||||
|
||||
## Deployment
|
||||
|
||||
Files are deployed to a **live production server** via FTP:
|
||||
- Host: `kikiriki.sklep.pl`
|
||||
- Remote path: `/domains/kikiriki.sklep.pl/public_html`
|
||||
- VS Code extension **ftp-kr** handles auto-upload on save (`autoUpload: true`)
|
||||
- Files excluded from FTP sync: `.git`, `.vscode`, `.claude`, `.serena`, `CLAUDE.md`
|
||||
|
||||
There is no local development server. Changes go directly to production.
|
||||
|
||||
## Architecture
|
||||
|
||||
PrestaShop uses a **class override system**: files in `override/` extend or replace core classes. After adding or removing an override, the cache file `/cache/class_index.php` on the server must be deleted to force regeneration.
|
||||
|
||||
### Key directories in this repo
|
||||
|
||||
- `config/` — PrestaShop configuration files (`settings.inc.php` has DB credentials, `defines.inc.php` sets dev/prod mode)
|
||||
- `controllers/admin/` and `controllers/front/` — Admin and storefront controller overrides
|
||||
- `override/classes/` — Core class overrides (currently only `Order.php` for InPost shipping integration)
|
||||
- `modules/` — Custom and third-party modules:
|
||||
- `eksporthistorii` — Custom module: exports order history to CSV, adds "Eksport historii" tab under Orders in admin
|
||||
- `ps_searchbar` — PrestaShop native search bar module (modified)
|
||||
- `x13googlemerchant` — Google Merchant Center XML feed (ionCube-encoded; main file is `xml.php`)
|
||||
- `themes/classic/` — Classic theme customizations (Smarty `.tpl` templates, assets, email templates in Polish)
|
||||
|
||||
### Module structure pattern
|
||||
|
||||
Each module follows the PrestaShop standard:
|
||||
- Main class in `<module_name>/<module_name>.php` extending `Module`
|
||||
- Admin controllers in `controllers/admin/Admin<Name>Controller.php` extending `ModuleAdminController`
|
||||
- Templates in `views/templates/`
|
||||
|
||||
### Override pattern
|
||||
|
||||
Override classes extend the `*Core` version of the class:
|
||||
```php
|
||||
class Order extends OrderCore { ... }
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
- **PS version**: `1.7.8.11` (defined in `config/settings.inc.php`)
|
||||
- **Dev mode**: disabled by default (`_PS_MODE_DEV_ = false` in `config/defines.inc.php`)
|
||||
- **DB prefix**: `pr_`
|
||||
- **Caching**: Memcache configured but disabled (`_PS_CACHE_ENABLED_ = '0'`)
|
||||
- **Language**: Polish (pl) — all custom UI strings are in Polish
|
||||
|
||||
## Important constraints
|
||||
|
||||
- `config/settings.inc.php` contains production DB credentials — never commit changes to this file that expose them further or modify the live credentials.
|
||||
- `x13googlemerchant` module files (`.core.php`, `.db.php`, `.schema.php`) are ionCube-encoded and cannot be edited directly. Only `x13googlemerchant.php` (the loader) and `xml.php` (the feed entry point) are plain PHP.
|
||||
- After editing any file in `override/`, the PrestaShop class cache must be cleared on the server: delete `/cache/class_index.php`.
|
||||
|
||||
## Sposób pracy
|
||||
- Pisz do mnie po polsku, zwięźle i krótko, ale merytorycznie
|
||||
|
||||
## Wprowadzanie zmian
|
||||
- Przeanalizuj wprowadzone zadanie
|
||||
- Jeżeli masz jakieś wątpliwości pytaj
|
||||
- Przedstaw plan
|
||||
- Po akceptacji wdróź plan
|
||||
@@ -26,8 +26,12 @@
|
||||
|
||||
/* Debug only */
|
||||
if (!defined('_PS_MODE_DEV_')) {
|
||||
if ( $_SERVER['REMOTE_ADDR'] === '91.189.216.43' ) {
|
||||
define('_PS_MODE_DEV_', true);
|
||||
} else {
|
||||
define('_PS_MODE_DEV_', false);
|
||||
}
|
||||
}
|
||||
/* Compatibility warning */
|
||||
define('_PS_DISPLAY_COMPATIBILITY_WARNING_', false);
|
||||
if (_PS_MODE_DEV_ === true) {
|
||||
|
||||
10
memory/MEMORY.md
Normal file
10
memory/MEMORY.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Pamięć projektu kikiriki.sklep.pl
|
||||
|
||||
## Aktywne zadania
|
||||
- **Moduł Blog** (`projectproblog`) — szczegółowy plan w `blog-module-plan.md`
|
||||
|
||||
## Kluczowe informacje
|
||||
- PrestaShop 1.7.8.11, DB prefix: `pr_`
|
||||
- Deploy: FTP auto-upload via ftp-kr (zapis = upload na produkcję)
|
||||
- Język UI: polski
|
||||
- Po każdej zmianie w `override/` → wyczyścić `/cache/class_index.php` na serwerze
|
||||
67
memory/blog-module-plan.md
Normal file
67
memory/blog-module-plan.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# Plan modułu Blog — Project-Pro Blog
|
||||
|
||||
## Dane modułu
|
||||
- Nazwa wyświetlana: `Project-Pro Blog`
|
||||
- Nazwa techniczna (katalog/klasa): `projectproblog`
|
||||
- Autor: `Project-Pro`
|
||||
- URL autora: `https://www.project-pro.pl`
|
||||
- PrestaShop: 1.7.8.11
|
||||
|
||||
## Etapy
|
||||
|
||||
### Etap 1 — Baza danych
|
||||
Tabele z prefiksem `pr_projectproblog_`:
|
||||
- `category` — id, id_parent, active, position, date_add, date_upd
|
||||
- `category_lang` — id_category, id_lang, name, description, link_rewrite, meta_title, meta_keywords, meta_description
|
||||
- `post` — id, active, thumbnail, date_add, date_upd
|
||||
- `post_lang` — id_post, id_lang, title, intro, content, link_rewrite, meta_title, meta_keywords, meta_description
|
||||
- `post_category` — id_post, id_category (relacja many-to-many)
|
||||
|
||||
### Etap 2 — Szkielet modułu (`projectproblog.php`)
|
||||
- Klasa `Projectproblog extends Module`
|
||||
- install() → SQL + taby admina + hooki
|
||||
- uninstall() → drop tabel + usunięcie tabów
|
||||
- Rejestracja ModuleFrontController (przyjazne URL-e)
|
||||
- Konfiguracja: liczba wpisów na stronę (domyślnie 9)
|
||||
|
||||
### Etap 3a — Admin: kategorie (`AdminProjectproBlogCategoriesController`)
|
||||
- Lista: nazwa, rodzic, aktywna, pozycja
|
||||
- Formularz (multilang): nazwa, opis, link_rewrite, meta_title/keywords/description
|
||||
- Pola: id_parent (select), active (toggle), position
|
||||
|
||||
### Etap 3b — Admin: wpisy (`AdminProjectproBlogPostsController`)
|
||||
- Lista: miniaturka, tytuł, kategorie, data dodania, aktywny
|
||||
- Formularz (multilang): tytuł, wstęp, treść (TinyMCE), link_rewrite, meta_title/keywords/description
|
||||
- Upload miniaturki z podglądem
|
||||
- Checkboxy przypisania do wielu kategorii
|
||||
- Toggle aktywny, daty auto
|
||||
|
||||
### Etap 4a — Front: lista (`list.php` ModuleFrontController)
|
||||
- URL: /blog i /blog/kategoria/{link_rewrite}
|
||||
- Filtrowanie po kategorii, stronicowanie
|
||||
- Dane do Smarty: wpisy, drzewo kategorii, bieżąca kategoria, paginacja
|
||||
|
||||
### Etap 4b — Front: szczegół (`post.php` ModuleFrontController)
|
||||
- URL: /blog/wpis/{link_rewrite}
|
||||
- Meta title/description/keywords w <head>
|
||||
- Dane: wpis, kategorie wpisu, breadcrumb
|
||||
|
||||
### Etap 5 — Szablony Smarty
|
||||
- `list.tpl` — layout 2-kolumnowy: lewa=drzewo kategorii, prawa=siatka wpisów 2-kol + paginacja
|
||||
- `post.tpl` — miniaturka, h1, wstęp wyróżniony, treść HTML, daty, breadcrumb
|
||||
- `_category-tree.tpl` — partial z drzewem (include w list.tpl)
|
||||
|
||||
### Etap 6 — Routing i SEO
|
||||
- Trasy: /blog, /blog/kategoria/{slug}, /blog/wpis/{slug}
|
||||
- link_rewrite generowany z tytułu
|
||||
- Canonical URL w szablonach
|
||||
|
||||
## Status etapów
|
||||
- [x] Etap 1 — Baza danych
|
||||
- [x] Etap 2 — Szkielet modułu
|
||||
- [x] Etap 3a — Admin kategorie
|
||||
- [x] Etap 3b — Admin wpisy
|
||||
- [x] Etap 4a — Front lista
|
||||
- [x] Etap 4b — Front szczegół
|
||||
- [x] Etap 5 — Szablony
|
||||
- [x] Etap 6 — Routing i SEO
|
||||
159
modules/projectproblog/classes/BlogCategory.php
Normal file
159
modules/projectproblog/classes/BlogCategory.php
Normal file
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
/**
|
||||
* Project-Pro Blog — BlogCategory ObjectModel
|
||||
*
|
||||
* @author Project-Pro <https://www.project-pro.pl>
|
||||
* @copyright 2024 Project-Pro
|
||||
*/
|
||||
|
||||
if (!defined('_PS_VERSION_')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class BlogCategory extends ObjectModel
|
||||
{
|
||||
/** @var int */
|
||||
public $id_parent = 0;
|
||||
|
||||
/** @var bool */
|
||||
public $active = true;
|
||||
|
||||
/** @var int */
|
||||
public $position = 0;
|
||||
|
||||
/** @var string */
|
||||
public $date_add;
|
||||
|
||||
/** @var string */
|
||||
public $date_upd;
|
||||
|
||||
// --- multilang ---
|
||||
|
||||
/** @var string */
|
||||
public $name;
|
||||
|
||||
/** @var string */
|
||||
public $description;
|
||||
|
||||
/** @var string */
|
||||
public $link_rewrite;
|
||||
|
||||
/** @var string */
|
||||
public $meta_title;
|
||||
|
||||
/** @var string */
|
||||
public $meta_keywords;
|
||||
|
||||
/** @var string */
|
||||
public $meta_description;
|
||||
|
||||
public static $definition = [
|
||||
'table' => 'projectproblog_category',
|
||||
'primary' => 'id_category',
|
||||
'multilang' => true,
|
||||
'fields' => [
|
||||
// główna tabela
|
||||
'id_parent' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'],
|
||||
'active' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
|
||||
'position' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'],
|
||||
'date_add' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'],
|
||||
'date_upd' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'],
|
||||
|
||||
// multilang
|
||||
'name' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'required' => true, 'size' => 255],
|
||||
'description' => ['type' => self::TYPE_HTML, 'lang' => true, 'validate' => 'isCleanHtml'],
|
||||
'link_rewrite' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isLinkRewrite', 'required' => true, 'size' => 255],
|
||||
'meta_title' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'size' => 255],
|
||||
'meta_keywords' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'size' => 255],
|
||||
'meta_description' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'size' => 512],
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Zwraca płaską listę kategorii do selecta w formularzu.
|
||||
* Format: [['id_category' => x, 'name' => '...'], ...]
|
||||
*/
|
||||
public static function getCategoriesForSelect($idLang, $currentId = 0)
|
||||
{
|
||||
$rows = Db::getInstance()->executeS(
|
||||
'SELECT c.`id_category`, cl.`name`
|
||||
FROM `' . _DB_PREFIX_ . 'projectproblog_category` c
|
||||
LEFT JOIN `' . _DB_PREFIX_ . 'projectproblog_category_lang` cl
|
||||
ON c.`id_category` = cl.`id_category` AND cl.`id_lang` = ' . (int) $idLang . '
|
||||
WHERE c.`id_category` != ' . (int) $currentId . '
|
||||
ORDER BY cl.`name` ASC'
|
||||
);
|
||||
|
||||
$result = [['id_category' => 0, 'name' => '— brak (kategoria główna) —']];
|
||||
if (is_array($rows)) {
|
||||
$result = array_merge($result, $rows);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zwraca drzewo kategorii do wyświetlenia na froncie.
|
||||
* Wynik: tablica zagnieżdżona ['category' => [...], 'children' => [...]]
|
||||
*/
|
||||
public static function getCategoryTree($idLang, $idParent = 0)
|
||||
{
|
||||
$rows = Db::getInstance()->executeS(
|
||||
'SELECT c.`id_category`, c.`id_parent`, c.`position`, cl.`name`, cl.`link_rewrite`
|
||||
FROM `' . _DB_PREFIX_ . 'projectproblog_category` c
|
||||
LEFT JOIN `' . _DB_PREFIX_ . 'projectproblog_category_lang` cl
|
||||
ON c.`id_category` = cl.`id_category` AND cl.`id_lang` = ' . (int) $idLang . '
|
||||
WHERE c.`active` = 1
|
||||
ORDER BY c.`position` ASC, cl.`name` ASC'
|
||||
);
|
||||
|
||||
if (!is_array($rows)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// indeks po id_parent
|
||||
$byParent = [];
|
||||
foreach ($rows as $row) {
|
||||
$byParent[(int) $row['id_parent']][] = $row;
|
||||
}
|
||||
|
||||
return self::buildTree($byParent, $idParent);
|
||||
}
|
||||
|
||||
private static function buildTree(array $byParent, $parentId)
|
||||
{
|
||||
if (!isset($byParent[$parentId])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$tree = [];
|
||||
foreach ($byParent[$parentId] as $cat) {
|
||||
$tree[] = [
|
||||
'category' => $cat,
|
||||
'children' => self::buildTree($byParent, (int) $cat['id_category']),
|
||||
];
|
||||
}
|
||||
|
||||
return $tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pobiera kategorię po link_rewrite dla bieżącego języka.
|
||||
*/
|
||||
public static function getByLinkRewrite($linkRewrite, $idLang)
|
||||
{
|
||||
$row = Db::getInstance()->getRow(
|
||||
'SELECT c.`id_category`
|
||||
FROM `' . _DB_PREFIX_ . 'projectproblog_category` c
|
||||
LEFT JOIN `' . _DB_PREFIX_ . 'projectproblog_category_lang` cl
|
||||
ON c.`id_category` = cl.`id_category` AND cl.`id_lang` = ' . (int) $idLang . '
|
||||
WHERE cl.`link_rewrite` = \'' . pSQL($linkRewrite) . '\' AND c.`active` = 1'
|
||||
);
|
||||
|
||||
if (!$row) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new BlogCategory((int) $row['id_category'], $idLang);
|
||||
}
|
||||
}
|
||||
219
modules/projectproblog/classes/BlogPost.php
Normal file
219
modules/projectproblog/classes/BlogPost.php
Normal file
@@ -0,0 +1,219 @@
|
||||
<?php
|
||||
/**
|
||||
* Project-Pro Blog — BlogPost ObjectModel
|
||||
*
|
||||
* @author Project-Pro <https://www.project-pro.pl>
|
||||
* @copyright 2024 Project-Pro
|
||||
*/
|
||||
|
||||
if (!defined('_PS_VERSION_')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class BlogPost extends ObjectModel
|
||||
{
|
||||
/** @var bool */
|
||||
public $active = true;
|
||||
|
||||
/** @var string|null */
|
||||
public $thumbnail;
|
||||
|
||||
/** @var string */
|
||||
public $date_add;
|
||||
|
||||
/** @var string */
|
||||
public $date_upd;
|
||||
|
||||
// --- multilang ---
|
||||
|
||||
/** @var string */
|
||||
public $title;
|
||||
|
||||
/** @var string */
|
||||
public $intro;
|
||||
|
||||
/** @var string */
|
||||
public $content;
|
||||
|
||||
/** @var string */
|
||||
public $link_rewrite;
|
||||
|
||||
/** @var string */
|
||||
public $meta_title;
|
||||
|
||||
/** @var string */
|
||||
public $meta_keywords;
|
||||
|
||||
/** @var string */
|
||||
public $meta_description;
|
||||
|
||||
public static $definition = [
|
||||
'table' => 'projectproblog_post',
|
||||
'primary' => 'id_post',
|
||||
'multilang' => true,
|
||||
'fields' => [
|
||||
// główna tabela
|
||||
'active' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
|
||||
'thumbnail' => ['type' => self::TYPE_STRING, 'validate' => 'isFileName', 'size' => 255],
|
||||
'date_add' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'],
|
||||
'date_upd' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'],
|
||||
|
||||
// multilang
|
||||
'title' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'required' => true, 'size' => 255],
|
||||
'intro' => ['type' => self::TYPE_HTML, 'lang' => true, 'validate' => 'isCleanHtml'],
|
||||
'content' => ['type' => self::TYPE_HTML, 'lang' => true, 'validate' => 'isCleanHtml'],
|
||||
'link_rewrite' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isLinkRewrite', 'required' => true, 'size' => 255],
|
||||
'meta_title' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'size' => 255],
|
||||
'meta_keywords' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'size' => 255],
|
||||
'meta_description' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'size' => 512],
|
||||
],
|
||||
];
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Delete — sprzątanie pliku i relacji z kategoriami */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
public function delete()
|
||||
{
|
||||
if ($this->thumbnail) {
|
||||
$file = _PS_MODULE_DIR_ . 'projectproblog/views/img/posts/' . $this->thumbnail;
|
||||
if (file_exists($file)) {
|
||||
@unlink($file);
|
||||
}
|
||||
}
|
||||
|
||||
Db::getInstance()->delete(
|
||||
'projectproblog_post_category',
|
||||
'id_post = ' . (int) $this->id
|
||||
);
|
||||
|
||||
return parent::delete();
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Relacje z kategoriami */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* Zwraca tablicę ID kategorii przypisanych do wpisu.
|
||||
*/
|
||||
public static function getCategories($idPost)
|
||||
{
|
||||
$rows = Db::getInstance()->executeS(
|
||||
'SELECT `id_category`
|
||||
FROM `' . _DB_PREFIX_ . 'projectproblog_post_category`
|
||||
WHERE `id_post` = ' . (int) $idPost
|
||||
);
|
||||
|
||||
return is_array($rows) ? array_column($rows, 'id_category') : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Zapisuje relacje wpis↔kategorie (zastępuje stare).
|
||||
*/
|
||||
public static function setCategories($idPost, array $categoryIds)
|
||||
{
|
||||
Db::getInstance()->delete(
|
||||
'projectproblog_post_category',
|
||||
'id_post = ' . (int) $idPost
|
||||
);
|
||||
|
||||
if (empty($categoryIds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
foreach ($categoryIds as $idCat) {
|
||||
if ((int) $idCat > 0) {
|
||||
$rows[] = [
|
||||
'id_post' => (int) $idPost,
|
||||
'id_category' => (int) $idCat,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ($rows) {
|
||||
Db::getInstance()->insert('projectproblog_post_category', $rows);
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Zapytania dla frontu */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* Lista wpisów do wyświetlenia na stronie (z opcjonalnym filtrem kategorii).
|
||||
*/
|
||||
public static function getList($idLang, $page, $perPage, $idCategory = null)
|
||||
{
|
||||
$page = max(1, (int) $page);
|
||||
$perPage = max(1, (int) $perPage);
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
$join = $idCategory
|
||||
? 'INNER JOIN `' . _DB_PREFIX_ . 'projectproblog_post_category` pc
|
||||
ON p.`id_post` = pc.`id_post` AND pc.`id_category` = ' . (int) $idCategory
|
||||
: '';
|
||||
|
||||
return Db::getInstance()->executeS(
|
||||
'SELECT p.`id_post`, p.`thumbnail`, p.`date_add`,
|
||||
pl.`title`, pl.`intro`, pl.`link_rewrite`
|
||||
FROM `' . _DB_PREFIX_ . 'projectproblog_post` p
|
||||
LEFT JOIN `' . _DB_PREFIX_ . 'projectproblog_post_lang` pl
|
||||
ON p.`id_post` = pl.`id_post` AND pl.`id_lang` = ' . (int) $idLang . '
|
||||
' . $join . '
|
||||
WHERE p.`active` = 1
|
||||
ORDER BY p.`date_add` DESC
|
||||
LIMIT ' . (int) $offset . ', ' . (int) $perPage
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Liczba wpisów (do paginacji).
|
||||
*/
|
||||
public static function getCount($idLang, $idCategory = null)
|
||||
{
|
||||
$join = $idCategory
|
||||
? 'INNER JOIN `' . _DB_PREFIX_ . 'projectproblog_post_category` pc
|
||||
ON p.`id_post` = pc.`id_post` AND pc.`id_category` = ' . (int) $idCategory
|
||||
: '';
|
||||
|
||||
return (int) Db::getInstance()->getValue(
|
||||
'SELECT COUNT(DISTINCT p.`id_post`)
|
||||
FROM `' . _DB_PREFIX_ . 'projectproblog_post` p
|
||||
' . $join . '
|
||||
WHERE p.`active` = 1'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pobiera wpis po link_rewrite dla danego języka.
|
||||
*/
|
||||
public static function getByLinkRewrite($linkRewrite, $idLang)
|
||||
{
|
||||
$row = Db::getInstance()->getRow(
|
||||
'SELECT p.`id_post`
|
||||
FROM `' . _DB_PREFIX_ . 'projectproblog_post` p
|
||||
INNER JOIN `' . _DB_PREFIX_ . 'projectproblog_post_lang` pl
|
||||
ON p.`id_post` = pl.`id_post` AND pl.`id_lang` = ' . (int) $idLang . '
|
||||
WHERE pl.`link_rewrite` = \'' . pSQL($linkRewrite) . '\' AND p.`active` = 1'
|
||||
);
|
||||
|
||||
return $row ? new BlogPost((int) $row['id_post'], $idLang) : null;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Helper URL miniaturki */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
public function getThumbnailUrl()
|
||||
{
|
||||
if (!$this->thumbnail) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Context::getContext()->link->getBaseLink()
|
||||
. 'modules/projectproblog/views/img/posts/'
|
||||
. $this->thumbnail;
|
||||
}
|
||||
}
|
||||
8
modules/projectproblog/classes/index.php
Normal file
8
modules/projectproblog/classes/index.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
|
||||
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate');
|
||||
header('Cache-Control: post-check=0, pre-check=0', false);
|
||||
header('Pragma: no-cache');
|
||||
header('Location: ../');
|
||||
exit;
|
||||
@@ -0,0 +1,201 @@
|
||||
<?php
|
||||
/**
|
||||
* Project-Pro Blog — Admin Categories Controller
|
||||
*
|
||||
* @author Project-Pro <https://www.project-pro.pl>
|
||||
* @copyright 2024 Project-Pro
|
||||
*/
|
||||
|
||||
if (!defined('_PS_VERSION_')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once _PS_MODULE_DIR_ . 'projectproblog/classes/BlogCategory.php';
|
||||
|
||||
class AdminProjectproBlogCategoriesController extends ModuleAdminController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->bootstrap = true;
|
||||
$this->table = 'projectproblog_category';
|
||||
$this->className = 'BlogCategory';
|
||||
$this->lang = true;
|
||||
$this->identifier = 'id_category';
|
||||
|
||||
$this->_defaultOrderBy = 'position';
|
||||
$this->_defaultOrderWay = 'ASC';
|
||||
|
||||
parent::__construct();
|
||||
|
||||
// JOIN musi być po parent::__construct() — wtedy $this->context jest już dostępny
|
||||
$this->_join =
|
||||
'LEFT JOIN `' . _DB_PREFIX_ . 'projectproblog_category_lang` parent_lang
|
||||
ON (a.`id_parent` = parent_lang.`id_category`
|
||||
AND parent_lang.`id_lang` = ' . (int) $this->context->language->id . ')';
|
||||
|
||||
$this->_select = 'parent_lang.`name` AS parent_name';
|
||||
|
||||
$this->addRowAction('edit');
|
||||
$this->addRowAction('delete');
|
||||
|
||||
/* -------------------------------------------------------- */
|
||||
/* Lista */
|
||||
/* -------------------------------------------------------- */
|
||||
$this->fields_list = [
|
||||
'id_category' => [
|
||||
'title' => $this->l('ID'),
|
||||
'align' => 'center',
|
||||
'class' => 'fixed-width-xs',
|
||||
],
|
||||
'name' => [
|
||||
'title' => $this->l('Nazwa'),
|
||||
'filter_key' => 'b!name',
|
||||
],
|
||||
'parent_name' => [
|
||||
'title' => $this->l('Kategoria nadrzędna'),
|
||||
'search' => false,
|
||||
],
|
||||
'position' => [
|
||||
'title' => $this->l('Pozycja'),
|
||||
'align' => 'center',
|
||||
'class' => 'fixed-width-xs',
|
||||
],
|
||||
'active' => [
|
||||
'title' => $this->l('Aktywna'),
|
||||
'active' => 'status',
|
||||
'type' => 'bool',
|
||||
'align' => 'center',
|
||||
'class' => 'fixed-width-sm',
|
||||
],
|
||||
];
|
||||
|
||||
/* -------------------------------------------------------- */
|
||||
/* Formularz */
|
||||
/* -------------------------------------------------------- */
|
||||
$this->fields_form = [
|
||||
'legend' => [
|
||||
'title' => $this->l('Kategoria bloga'),
|
||||
'icon' => 'icon-folder',
|
||||
],
|
||||
'input' => [
|
||||
[
|
||||
'type' => 'select',
|
||||
'label' => $this->l('Kategoria nadrzędna'),
|
||||
'name' => 'id_parent',
|
||||
'options' => [
|
||||
'query' => BlogCategory::getCategoriesForSelect(
|
||||
$this->context->language->id,
|
||||
(int) Tools::getValue('id_category')
|
||||
),
|
||||
'id' => 'id_category',
|
||||
'name' => 'name',
|
||||
],
|
||||
'hint' => $this->l('Zostaw "brak" jeśli to kategoria główna.'),
|
||||
],
|
||||
[
|
||||
'type' => 'switch',
|
||||
'label' => $this->l('Aktywna'),
|
||||
'name' => 'active',
|
||||
'required' => false,
|
||||
'is_bool' => true,
|
||||
'values' => [
|
||||
['id' => 'active_on', 'value' => 1, 'label' => $this->l('Tak')],
|
||||
['id' => 'active_off', 'value' => 0, 'label' => $this->l('Nie')],
|
||||
],
|
||||
],
|
||||
[
|
||||
'type' => 'text',
|
||||
'label' => $this->l('Pozycja'),
|
||||
'name' => 'position',
|
||||
'class' => 'fixed-width-xs',
|
||||
'hint' => $this->l('Kolejność na liście (mniejsza liczba = wyżej).'),
|
||||
],
|
||||
[
|
||||
'type' => 'text',
|
||||
'label' => $this->l('Nazwa'),
|
||||
'name' => 'name',
|
||||
'lang' => true,
|
||||
'required' => true,
|
||||
'hint' => $this->l('Nazwa wyświetlana na stronie.'),
|
||||
],
|
||||
[
|
||||
'type' => 'text',
|
||||
'label' => $this->l('Przyjazny URL (slug)'),
|
||||
'name' => 'link_rewrite',
|
||||
'lang' => true,
|
||||
'required' => true,
|
||||
'hint' => $this->l('Tylko małe litery, cyfry i myślniki. Generowany automatycznie z nazwy.'),
|
||||
],
|
||||
[
|
||||
'type' => 'textarea',
|
||||
'label' => $this->l('Opis'),
|
||||
'name' => 'description',
|
||||
'lang' => true,
|
||||
'rows' => 5,
|
||||
],
|
||||
[
|
||||
'type' => 'text',
|
||||
'label' => $this->l('Meta title'),
|
||||
'name' => 'meta_title',
|
||||
'lang' => true,
|
||||
],
|
||||
[
|
||||
'type' => 'text',
|
||||
'label' => $this->l('Meta keywords'),
|
||||
'name' => 'meta_keywords',
|
||||
'lang' => true,
|
||||
'hint' => $this->l('Słowa kluczowe oddzielone przecinkami.'),
|
||||
],
|
||||
[
|
||||
'type' => 'textarea',
|
||||
'label' => $this->l('Meta description'),
|
||||
'name' => 'meta_description',
|
||||
'lang' => true,
|
||||
'rows' => 3,
|
||||
],
|
||||
],
|
||||
'submit' => [
|
||||
'title' => $this->l('Zapisz'),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Zapis — auto-generowanie slug z nazwy */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
public function processSave()
|
||||
{
|
||||
$languages = Language::getLanguages(false);
|
||||
|
||||
foreach ($languages as $lang) {
|
||||
$idLang = (int) $lang['id_lang'];
|
||||
$name = Tools::getValue('name_' . $idLang);
|
||||
$slug = Tools::getValue('link_rewrite_' . $idLang);
|
||||
|
||||
// jeśli slug pusty — wygeneruj z nazwy
|
||||
if (empty(trim((string) $slug)) && !empty(trim((string) $name))) {
|
||||
$_POST['link_rewrite_' . $idLang] = Tools::link_rewrite($name);
|
||||
}
|
||||
}
|
||||
|
||||
return parent::processSave();
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Toggle active (kliknięcie boolki na liście) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
public function processStatus()
|
||||
{
|
||||
$cat = new BlogCategory((int) Tools::getValue($this->identifier));
|
||||
if (!Validate::isLoadedObject($cat)) {
|
||||
$this->errors[] = $this->l('Nie znaleziono kategorii.');
|
||||
return false;
|
||||
}
|
||||
|
||||
$cat->active = !(bool) $cat->active;
|
||||
|
||||
return (bool) $cat->update();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
<?php
|
||||
/**
|
||||
* Project-Pro Blog — Admin Posts Controller
|
||||
*
|
||||
* @author Project-Pro <https://www.project-pro.pl>
|
||||
* @copyright 2024 Project-Pro
|
||||
*/
|
||||
|
||||
if (!defined('_PS_VERSION_')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once _PS_MODULE_DIR_ . 'projectproblog/classes/BlogPost.php';
|
||||
require_once _PS_MODULE_DIR_ . 'projectproblog/classes/BlogCategory.php';
|
||||
|
||||
class AdminProjectproBlogPostsController extends ModuleAdminController
|
||||
{
|
||||
/** Katalog przechowywania miniaturek */
|
||||
const IMG_DIR = 'projectproblog/views/img/posts/';
|
||||
|
||||
/** Flaga zapobiegająca podwójnemu wstrzyknięciu pól do formularza */
|
||||
private $formEnriched = false;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->bootstrap = true;
|
||||
$this->table = 'projectproblog_post';
|
||||
$this->className = 'BlogPost';
|
||||
$this->lang = true;
|
||||
$this->identifier = 'id_post';
|
||||
|
||||
$this->_defaultOrderBy = 'date_add';
|
||||
$this->_defaultOrderWay = 'DESC';
|
||||
|
||||
parent::__construct();
|
||||
|
||||
$this->addRowAction('edit');
|
||||
$this->addRowAction('delete');
|
||||
|
||||
/* -------------------------------------------------------- */
|
||||
/* Lista */
|
||||
/* -------------------------------------------------------- */
|
||||
$this->fields_list = [
|
||||
'id_post' => [
|
||||
'title' => $this->l('ID'),
|
||||
'align' => 'center',
|
||||
'class' => 'fixed-width-xs',
|
||||
],
|
||||
'title' => [
|
||||
'title' => $this->l('Tytuł'),
|
||||
'filter_key' => 'b!title',
|
||||
],
|
||||
'date_add' => [
|
||||
'title' => $this->l('Data dodania'),
|
||||
'type' => 'datetime',
|
||||
'align' => 'center',
|
||||
],
|
||||
'active' => [
|
||||
'title' => $this->l('Aktywny'),
|
||||
'active' => 'status',
|
||||
'type' => 'bool',
|
||||
'align' => 'center',
|
||||
'class' => 'fixed-width-sm',
|
||||
],
|
||||
];
|
||||
|
||||
/* -------------------------------------------------------- */
|
||||
/* Formularz — pola statyczne */
|
||||
/* (miniaturka i kategorie dołączane dynamicznie */
|
||||
/* w renderForm() żeby obsłużyć podgląd i zaznaczenia) */
|
||||
/* -------------------------------------------------------- */
|
||||
$this->fields_form = [
|
||||
'legend' => [
|
||||
'title' => $this->l('Wpis bloga'),
|
||||
'icon' => 'icon-edit',
|
||||
],
|
||||
'input' => [
|
||||
[
|
||||
'type' => 'switch',
|
||||
'label' => $this->l('Aktywny'),
|
||||
'name' => 'active',
|
||||
'required' => false,
|
||||
'is_bool' => true,
|
||||
'values' => [
|
||||
['id' => 'active_on', 'value' => 1, 'label' => $this->l('Tak')],
|
||||
['id' => 'active_off', 'value' => 0, 'label' => $this->l('Nie')],
|
||||
],
|
||||
],
|
||||
[
|
||||
'type' => 'text',
|
||||
'label' => $this->l('Tytuł'),
|
||||
'name' => 'title',
|
||||
'lang' => true,
|
||||
'required' => true,
|
||||
'col' => 6,
|
||||
],
|
||||
[
|
||||
'type' => 'text',
|
||||
'label' => $this->l('Przyjazny URL (slug)'),
|
||||
'name' => 'link_rewrite',
|
||||
'lang' => true,
|
||||
'required' => true,
|
||||
'col' => 6,
|
||||
'hint' => $this->l('Tylko małe litery, cyfry i myślniki. Generowany automatycznie z tytułu.'),
|
||||
],
|
||||
[
|
||||
'type' => 'textarea',
|
||||
'label' => $this->l('Wstęp'),
|
||||
'name' => 'intro',
|
||||
'lang' => true,
|
||||
'rows' => 5,
|
||||
'autoload_rte' => true,
|
||||
'hint' => $this->l('Krótki opis wyświetlany na liście wpisów.'),
|
||||
],
|
||||
[
|
||||
'type' => 'textarea',
|
||||
'label' => $this->l('Treść'),
|
||||
'name' => 'content',
|
||||
'lang' => true,
|
||||
'rows' => 20,
|
||||
'autoload_rte' => true,
|
||||
],
|
||||
[
|
||||
'type' => 'text',
|
||||
'label' => $this->l('Meta title'),
|
||||
'name' => 'meta_title',
|
||||
'lang' => true,
|
||||
'col' => 6,
|
||||
],
|
||||
[
|
||||
'type' => 'text',
|
||||
'label' => $this->l('Meta keywords'),
|
||||
'name' => 'meta_keywords',
|
||||
'lang' => true,
|
||||
'col' => 6,
|
||||
'hint' => $this->l('Słowa kluczowe oddzielone przecinkami.'),
|
||||
],
|
||||
[
|
||||
'type' => 'textarea',
|
||||
'label' => $this->l('Meta description'),
|
||||
'name' => 'meta_description',
|
||||
'lang' => true,
|
||||
'rows' => 3,
|
||||
],
|
||||
// miniaturka i kategorie → renderForm()
|
||||
],
|
||||
'submit' => [
|
||||
'title' => $this->l('Zapisz'),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Formularz — dynamiczne pola: miniaturka + kategorie */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
public function renderForm()
|
||||
{
|
||||
// Ochrona przed podwójnym wstrzyknięciem (np. przy re-renderze po błędzie walidacji)
|
||||
if ($this->formEnriched) {
|
||||
return parent::renderForm();
|
||||
}
|
||||
$this->formEnriched = true;
|
||||
|
||||
$idPost = (int) Tools::getValue('id_post');
|
||||
$existingThumbnail = null;
|
||||
$selectedCategories = [];
|
||||
|
||||
if ($idPost) {
|
||||
$post = new BlogPost($idPost);
|
||||
if (Validate::isLoadedObject($post)) {
|
||||
$existingThumbnail = $post->thumbnail;
|
||||
$selectedCategories = array_map('intval', BlogPost::getCategories($idPost));
|
||||
}
|
||||
}
|
||||
|
||||
// --- pole miniaturki ---
|
||||
$thumbnailHtml = '<div class="form-group">';
|
||||
$thumbnailHtml .= '<label class="control-label col-lg-3">' . $this->l('Miniaturka') . '</label>';
|
||||
$thumbnailHtml .= '<div class="col-lg-9">';
|
||||
|
||||
if ($existingThumbnail) {
|
||||
$url = Context::getContext()->link->getBaseLink()
|
||||
. 'modules/projectproblog/views/img/posts/'
|
||||
. $existingThumbnail;
|
||||
$thumbnailHtml .= '<div style="margin-bottom:10px;">'
|
||||
. '<img src="' . $url . '" style="max-height:150px;max-width:300px;border:1px solid #ddd;padding:4px;" />'
|
||||
. '</div>';
|
||||
}
|
||||
|
||||
$thumbnailHtml .= '<input type="file" name="thumbnail" id="thumbnail_upload" '
|
||||
. 'accept="image/jpeg,image/png,image/webp,image/gif" class="form-control-file">';
|
||||
$thumbnailHtml .= '<p class="help-block">' . $this->l('Dozwolone formaty: jpg, png, webp, gif.') . '</p>';
|
||||
$thumbnailHtml .= '</div></div>';
|
||||
|
||||
// --- pole kategorii ---
|
||||
$allCategories = Db::getInstance()->executeS(
|
||||
'SELECT c.`id_category`, cl.`name`
|
||||
FROM `' . _DB_PREFIX_ . 'projectproblog_category` c
|
||||
LEFT JOIN `' . _DB_PREFIX_ . 'projectproblog_category_lang` cl
|
||||
ON c.`id_category` = cl.`id_category`
|
||||
AND cl.`id_lang` = ' . (int) $this->context->language->id . '
|
||||
WHERE c.`active` = 1
|
||||
ORDER BY cl.`name` ASC'
|
||||
);
|
||||
|
||||
$categoriesHtml = '<div class="form-group">';
|
||||
$categoriesHtml .= '<label class="control-label col-lg-3">'
|
||||
. $this->l('Kategorie')
|
||||
. ' <span class="help-box" data-toggle="tooltip" title="'
|
||||
. $this->l('Wpis może być przypisany do wielu kategorii.') . '"></span>'
|
||||
. '</label>';
|
||||
$categoriesHtml .= '<div class="col-lg-9">';
|
||||
|
||||
if (!empty($allCategories)) {
|
||||
$categoriesHtml .= '<div style="max-height:200px;overflow-y:auto;border:1px solid #ddd;padding:10px;background:#fff;">';
|
||||
foreach ($allCategories as $cat) {
|
||||
$checked = in_array((int) $cat['id_category'], $selectedCategories) ? ' checked' : '';
|
||||
$categoriesHtml .= '<div class="checkbox" style="margin:4px 0;">'
|
||||
. '<label>'
|
||||
. '<input type="checkbox" name="id_category[]" value="' . (int) $cat['id_category'] . '"' . $checked . '> '
|
||||
. htmlspecialchars($cat['name'], ENT_QUOTES, 'UTF-8')
|
||||
. '</label>'
|
||||
. '</div>';
|
||||
}
|
||||
$categoriesHtml .= '</div>';
|
||||
} else {
|
||||
$categoriesHtml .= '<p class="text-muted">'
|
||||
. $this->l('Brak aktywnych kategorii. Dodaj kategorie w zakładce "Kategorie bloga".')
|
||||
. '</p>';
|
||||
}
|
||||
|
||||
$categoriesHtml .= '</div></div>';
|
||||
|
||||
// --- wstrzykiwanie do formularza ---
|
||||
$this->fields_form['input'][] = [
|
||||
'type' => 'html',
|
||||
'label' => '',
|
||||
'name' => 'thumbnail_field',
|
||||
'html_content' => $thumbnailHtml,
|
||||
];
|
||||
|
||||
$this->fields_form['input'][] = [
|
||||
'type' => 'html',
|
||||
'label' => '',
|
||||
'name' => 'categories_field',
|
||||
'html_content' => $categoriesHtml,
|
||||
];
|
||||
|
||||
return parent::renderForm();
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Zapis */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
public function processSave()
|
||||
{
|
||||
$idPost = (int) Tools::getValue('id_post');
|
||||
$imgDir = _PS_MODULE_DIR_ . self::IMG_DIR;
|
||||
|
||||
// --- obsługa miniaturki ---
|
||||
if (!empty($_FILES['thumbnail']['name']) && $_FILES['thumbnail']['error'] === UPLOAD_ERR_OK) {
|
||||
|
||||
if (!is_dir($imgDir)) {
|
||||
@mkdir($imgDir, 0755, true);
|
||||
}
|
||||
|
||||
$ext = strtolower(pathinfo($_FILES['thumbnail']['name'], PATHINFO_EXTENSION));
|
||||
$allowed = ['jpg', 'jpeg', 'png', 'webp', 'gif'];
|
||||
|
||||
if (!in_array($ext, $allowed)) {
|
||||
$this->errors[] = $this->l('Niedozwolony format pliku. Dozwolone: jpg, png, webp, gif.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (@getimagesize($_FILES['thumbnail']['tmp_name']) === false) {
|
||||
$this->errors[] = $this->l('Przesłany plik nie jest prawidłowym obrazem.');
|
||||
return false;
|
||||
}
|
||||
|
||||
$filename = md5(uniqid('ppb', true)) . '.' . $ext;
|
||||
|
||||
if (!move_uploaded_file($_FILES['thumbnail']['tmp_name'], $imgDir . $filename)) {
|
||||
$this->errors[] = $this->l('Błąd podczas przesyłania pliku. Sprawdź uprawnienia katalogu.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// usuń starą miniaturkę przy edycji
|
||||
if ($idPost) {
|
||||
$old = new BlogPost($idPost);
|
||||
if (Validate::isLoadedObject($old) && $old->thumbnail) {
|
||||
$oldFile = $imgDir . $old->thumbnail;
|
||||
if (file_exists($oldFile)) {
|
||||
@unlink($oldFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$_POST['thumbnail'] = $filename;
|
||||
|
||||
} else {
|
||||
// brak nowego pliku — zachowaj istniejącą miniaturkę
|
||||
if ($idPost) {
|
||||
$existing = new BlogPost($idPost);
|
||||
if (Validate::isLoadedObject($existing) && $existing->thumbnail) {
|
||||
$_POST['thumbnail'] = $existing->thumbnail;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- auto-generowanie slugów z tytułu ---
|
||||
foreach (Language::getLanguages(false) as $lang) {
|
||||
$lid = (int) $lang['id_lang'];
|
||||
$title = trim((string) Tools::getValue('title_' . $lid));
|
||||
$slug = trim((string) Tools::getValue('link_rewrite_' . $lid));
|
||||
|
||||
if ($slug === '' && $title !== '') {
|
||||
$_POST['link_rewrite_' . $lid] = Tools::link_rewrite($title);
|
||||
}
|
||||
}
|
||||
|
||||
// --- zapis przez parent (ObjectModel) ---
|
||||
$result = parent::processSave();
|
||||
|
||||
// --- zapis kategorii ---
|
||||
if ($result !== false && isset($this->object) && Validate::isLoadedObject($this->object)) {
|
||||
$categoryIds = Tools::getValue('id_category', []);
|
||||
if (!is_array($categoryIds)) {
|
||||
$categoryIds = [];
|
||||
}
|
||||
BlogPost::setCategories((int) $this->object->id, $categoryIds);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Toggle aktywności z listy */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
public function processStatus()
|
||||
{
|
||||
$post = new BlogPost((int) Tools::getValue($this->identifier));
|
||||
|
||||
if (!Validate::isLoadedObject($post)) {
|
||||
$this->errors[] = $this->l('Nie znaleziono wpisu.');
|
||||
return false;
|
||||
}
|
||||
|
||||
$post->active = !(bool) $post->active;
|
||||
|
||||
return (bool) $post->update();
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Usunięcie — thumbnail i kategorie obsługuje BlogPost::delete() */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
public function processDelete()
|
||||
{
|
||||
$post = new BlogPost((int) Tools::getValue($this->identifier));
|
||||
|
||||
if (!Validate::isLoadedObject($post)) {
|
||||
$this->errors[] = $this->l('Nie znaleziono wpisu.');
|
||||
return false;
|
||||
}
|
||||
|
||||
$result = $post->delete();
|
||||
|
||||
if (!$result) {
|
||||
$this->errors[] = $this->l('Wystąpił błąd podczas usuwania wpisu.');
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
8
modules/projectproblog/controllers/admin/index.php
Normal file
8
modules/projectproblog/controllers/admin/index.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
|
||||
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
|
||||
header("Cache-Control: no-store, no-cache, must-revalidate");
|
||||
header("Cache-Control: post-check=0, pre-check=0", false);
|
||||
header("Pragma: no-cache");
|
||||
header("Location: ../");
|
||||
exit;
|
||||
8
modules/projectproblog/controllers/front/index.php
Normal file
8
modules/projectproblog/controllers/front/index.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
|
||||
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
|
||||
header("Cache-Control: no-store, no-cache, must-revalidate");
|
||||
header("Cache-Control: post-check=0, pre-check=0", false);
|
||||
header("Pragma: no-cache");
|
||||
header("Location: ../");
|
||||
exit;
|
||||
252
modules/projectproblog/controllers/front/list.php
Normal file
252
modules/projectproblog/controllers/front/list.php
Normal file
@@ -0,0 +1,252 @@
|
||||
<?php
|
||||
/**
|
||||
* Project-Pro Blog — Front controller: lista wpisów
|
||||
*
|
||||
* @author Project-Pro <https://www.project-pro.pl>
|
||||
* @copyright 2024 Project-Pro
|
||||
*/
|
||||
|
||||
if (!defined('_PS_VERSION_')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once _PS_MODULE_DIR_ . 'projectproblog/classes/BlogCategory.php';
|
||||
require_once _PS_MODULE_DIR_ . 'projectproblog/classes/BlogPost.php';
|
||||
|
||||
class ProjectproblogListModuleFrontController extends ModuleFrontController
|
||||
{
|
||||
public $php_self = 'list';
|
||||
|
||||
/** @var BlogCategory|null */
|
||||
protected $currentCategory = null;
|
||||
|
||||
/** @var string */
|
||||
protected $slug = '';
|
||||
|
||||
public function init()
|
||||
{
|
||||
parent::init();
|
||||
|
||||
$idLang = (int) $this->context->language->id;
|
||||
$this->slug = Tools::getValue('slug', '');
|
||||
|
||||
// Jeśli podano slug kategorii — rozwiąż go
|
||||
if ($this->slug !== '') {
|
||||
$this->currentCategory = BlogCategory::getByLinkRewrite($this->slug, $idLang);
|
||||
if (!$this->currentCategory) {
|
||||
Tools::redirect('index.php?controller=404');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function initContent()
|
||||
{
|
||||
parent::initContent();
|
||||
|
||||
$this->registerStylesheet(
|
||||
'module-projectproblog-blog',
|
||||
'modules/projectproblog/views/css/blog.css',
|
||||
['media' => 'all', 'priority' => 200]
|
||||
);
|
||||
|
||||
$idLang = (int) $this->context->language->id;
|
||||
$page = max(1, (int) Tools::getValue('page', 1));
|
||||
$perPage = Projectproblog::POSTS_PER_PAGE;
|
||||
$idCategory = $this->currentCategory ? (int) $this->currentCategory->id : null;
|
||||
|
||||
// Wpisy
|
||||
$rawPosts = BlogPost::getList($idLang, $page, $perPage, $idCategory);
|
||||
$total = BlogPost::getCount($idLang, $idCategory);
|
||||
$pages = max(1, (int) ceil($total / $perPage));
|
||||
|
||||
// Wzbogac wpisy o URL-e
|
||||
$posts = $this->enrichPosts($rawPosts, $idLang);
|
||||
|
||||
// Paginacja
|
||||
$pagination = $this->buildPagination($page, $pages, $this->slug, $idLang);
|
||||
|
||||
// Drzewo kategorii z URL-ami
|
||||
$categoryTree = $this->enrichCategoryTree(
|
||||
BlogCategory::getCategoryTree($idLang),
|
||||
$idLang
|
||||
);
|
||||
|
||||
// Meta strony
|
||||
$this->setPageMeta($idLang);
|
||||
|
||||
$this->context->smarty->assign([
|
||||
'blog_posts' => $posts,
|
||||
'blog_category_tree' => $categoryTree,
|
||||
'blog_current_cat' => $this->currentCategory,
|
||||
'blog_pagination' => $pagination,
|
||||
'blog_page' => $page,
|
||||
'blog_pages_count' => $pages,
|
||||
'blog_total' => $total,
|
||||
'blog_url' => Projectproblog::getBlogUrl($idLang),
|
||||
]);
|
||||
|
||||
$this->setTemplate('module:projectproblog/views/templates/front/list.tpl');
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Breadcrumb */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
public function getBreadcrumbLinks()
|
||||
{
|
||||
$breadcrumb = parent::getBreadcrumbLinks();
|
||||
|
||||
$breadcrumb['links'][] = [
|
||||
'title' => $this->l('Blog'),
|
||||
'url' => Projectproblog::getBlogUrl(),
|
||||
];
|
||||
|
||||
if ($this->currentCategory) {
|
||||
$breadcrumb['links'][] = [
|
||||
'title' => $this->currentCategory->name,
|
||||
'url' => Projectproblog::getCategoryUrl($this->currentCategory->link_rewrite),
|
||||
];
|
||||
}
|
||||
|
||||
return $breadcrumb;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Canonical URL */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* Wyłącza canonical redirection — PS generuje błędny fallback URL
|
||||
* (bez fc=module) gdy hookModuleRoutes nie załadował tras w Dispatcher.
|
||||
*/
|
||||
protected function canonicalRedirection($canonical_url = '')
|
||||
{
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Helpery prywatne */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* Dodaje thumbnail_url i url do każdego wpisu.
|
||||
*/
|
||||
protected function enrichPosts(array $posts, $idLang)
|
||||
{
|
||||
$base = $this->context->link->getBaseLink();
|
||||
|
||||
foreach ($posts as &$post) {
|
||||
$post['thumbnail_url'] = $post['thumbnail']
|
||||
? $base . 'modules/projectproblog/views/img/posts/' . $post['thumbnail']
|
||||
: null;
|
||||
|
||||
$post['url'] = Projectproblog::getPostUrl($post['link_rewrite'], $idLang);
|
||||
}
|
||||
unset($post);
|
||||
|
||||
return $posts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rekurencyjnie dodaje url do węzłów drzewa kategorii.
|
||||
*/
|
||||
protected function enrichCategoryTree(array $tree, $idLang)
|
||||
{
|
||||
foreach ($tree as &$node) {
|
||||
$node['category']['url'] = Projectproblog::getCategoryUrl(
|
||||
$node['category']['link_rewrite'],
|
||||
$idLang
|
||||
);
|
||||
$node['children'] = $this->enrichCategoryTree($node['children'], $idLang);
|
||||
}
|
||||
unset($node);
|
||||
|
||||
return $tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* Buduje tablicę linków paginacji.
|
||||
* Każdy element: ['page' => int, 'url' => string, 'current' => bool]
|
||||
*/
|
||||
protected function buildPagination($currentPage, $pagesCount, $slug, $idLang)
|
||||
{
|
||||
if ($pagesCount <= 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$links = [];
|
||||
|
||||
for ($i = 1; $i <= $pagesCount; $i++) {
|
||||
$params = [];
|
||||
if ($slug !== '') {
|
||||
$params['slug'] = $slug;
|
||||
}
|
||||
if ($i > 1) {
|
||||
$params['page'] = $i;
|
||||
}
|
||||
|
||||
$links[] = [
|
||||
'page' => $i,
|
||||
'url' => $this->context->link->getModuleLink(
|
||||
'projectproblog',
|
||||
'list',
|
||||
$params,
|
||||
true,
|
||||
$idLang
|
||||
),
|
||||
'current' => ($i === $currentPage),
|
||||
];
|
||||
}
|
||||
|
||||
// Linki poprzednia/następna
|
||||
$prev = null;
|
||||
$next = null;
|
||||
|
||||
if ($currentPage > 1) {
|
||||
$params = $slug !== '' ? ['slug' => $slug] : [];
|
||||
if ($currentPage - 1 > 1) {
|
||||
$params['page'] = $currentPage - 1;
|
||||
}
|
||||
$prev = $this->context->link->getModuleLink('projectproblog', 'list', $params, true, $idLang);
|
||||
}
|
||||
|
||||
if ($currentPage < $pagesCount) {
|
||||
$params = $slug !== '' ? ['slug' => $slug] : [];
|
||||
$params['page'] = $currentPage + 1;
|
||||
$next = $this->context->link->getModuleLink('projectproblog', 'list', $params, true, $idLang);
|
||||
}
|
||||
|
||||
return [
|
||||
'pages' => $links,
|
||||
'prev' => $prev,
|
||||
'next' => $next,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ustawia meta title/description strony listy.
|
||||
*/
|
||||
protected function setPageMeta($idLang)
|
||||
{
|
||||
if ($this->currentCategory) {
|
||||
$title = $this->currentCategory->meta_title ?: $this->currentCategory->name . ' — Blog';
|
||||
$desc = $this->currentCategory->meta_description ?: '';
|
||||
$kw = $this->currentCategory->meta_keywords ?: '';
|
||||
} else {
|
||||
$title = $this->l('Blog');
|
||||
$desc = '';
|
||||
$kw = '';
|
||||
}
|
||||
|
||||
$page = $this->context->smarty->getTemplateVars('page') ?: [];
|
||||
|
||||
if (!isset($page['meta'])) {
|
||||
$page['meta'] = [];
|
||||
}
|
||||
|
||||
$page['meta']['title'] = $title;
|
||||
$page['meta']['description'] = $desc;
|
||||
$page['meta']['keywords'] = $kw;
|
||||
|
||||
$this->context->smarty->assign('page', $page);
|
||||
}
|
||||
}
|
||||
225
modules/projectproblog/controllers/front/post.php
Normal file
225
modules/projectproblog/controllers/front/post.php
Normal file
@@ -0,0 +1,225 @@
|
||||
<?php
|
||||
/**
|
||||
* Project-Pro Blog — Front controller: szczegół wpisu
|
||||
*
|
||||
* @author Project-Pro <https://www.project-pro.pl>
|
||||
* @copyright 2024 Project-Pro
|
||||
*/
|
||||
|
||||
if (!defined('_PS_VERSION_')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once _PS_MODULE_DIR_ . 'projectproblog/classes/BlogCategory.php';
|
||||
require_once _PS_MODULE_DIR_ . 'projectproblog/classes/BlogPost.php';
|
||||
|
||||
class ProjectproblogPostModuleFrontController extends ModuleFrontController
|
||||
{
|
||||
public $php_self = 'post';
|
||||
|
||||
/** @var BlogPost|null */
|
||||
protected $post = null;
|
||||
|
||||
/** @var string */
|
||||
protected $slug = '';
|
||||
|
||||
public function init()
|
||||
{
|
||||
parent::init();
|
||||
|
||||
$idLang = (int) $this->context->language->id;
|
||||
$this->slug = Tools::getValue('slug', '');
|
||||
|
||||
if ($this->slug === '') {
|
||||
Tools::redirect('index.php?controller=404');
|
||||
}
|
||||
|
||||
$this->post = BlogPost::getByLinkRewrite($this->slug, $idLang);
|
||||
|
||||
if (!$this->post) {
|
||||
Tools::redirect('index.php?controller=404');
|
||||
}
|
||||
}
|
||||
|
||||
public function initContent()
|
||||
{
|
||||
parent::initContent();
|
||||
|
||||
$this->registerStylesheet(
|
||||
'module-projectproblog-blog',
|
||||
'modules/projectproblog/views/css/blog.css',
|
||||
['media' => 'all', 'priority' => 200]
|
||||
);
|
||||
|
||||
$idLang = (int) $this->context->language->id;
|
||||
|
||||
// Kategorie wpisu z URL-ami
|
||||
$categoryIds = BlogPost::getCategories((int) $this->post->id);
|
||||
$postCats = $this->loadCategories($categoryIds, $idLang);
|
||||
|
||||
// Pierwsza kategoria (do breadcrumba)
|
||||
$primaryCat = !empty($postCats) ? $postCats[0] : null;
|
||||
|
||||
// URL miniaturki
|
||||
$thumbnailUrl = $this->post->getThumbnailUrl();
|
||||
|
||||
// Powiązane data_upd — wyświetl tylko gdy różni się od data_add
|
||||
$showUpdated = $this->post->date_upd
|
||||
&& substr($this->post->date_upd, 0, 10) !== substr($this->post->date_add, 0, 10);
|
||||
|
||||
// Drzewo kategorii do sidebara
|
||||
$categoryTree = $this->enrichCategoryTree(
|
||||
BlogCategory::getCategoryTree($idLang),
|
||||
$idLang
|
||||
);
|
||||
|
||||
// Meta strony
|
||||
$this->setPageMeta();
|
||||
|
||||
$this->context->smarty->assign([
|
||||
'blog_post' => $this->post,
|
||||
'blog_post_cats' => $postCats,
|
||||
'blog_primary_cat' => $primaryCat,
|
||||
'blog_thumbnail' => $thumbnailUrl,
|
||||
'blog_show_updated' => $showUpdated,
|
||||
'blog_url' => Projectproblog::getBlogUrl($idLang),
|
||||
'blog_category_tree' => $categoryTree,
|
||||
'blog_current_cat' => null,
|
||||
]);
|
||||
|
||||
$this->setTemplate('module:projectproblog/views/templates/front/post.tpl');
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Breadcrumb */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
public function getBreadcrumbLinks()
|
||||
{
|
||||
$breadcrumb = parent::getBreadcrumbLinks();
|
||||
|
||||
$breadcrumb['links'][] = [
|
||||
'title' => $this->l('Blog'),
|
||||
'url' => Projectproblog::getBlogUrl(),
|
||||
];
|
||||
|
||||
// Jeśli wpis ma kategorię — dodaj ją do breadcrumba
|
||||
if ($this->post) {
|
||||
$idLang = (int) $this->context->language->id;
|
||||
$categoryIds = BlogPost::getCategories((int) $this->post->id);
|
||||
|
||||
if (!empty($categoryIds)) {
|
||||
$catRow = Db::getInstance()->getRow(
|
||||
'SELECT cl.`name`, cl.`link_rewrite`
|
||||
FROM `' . _DB_PREFIX_ . 'projectproblog_category_lang` cl
|
||||
WHERE cl.`id_category` = ' . (int) $categoryIds[0] . '
|
||||
AND cl.`id_lang` = ' . (int) $idLang
|
||||
);
|
||||
|
||||
if ($catRow) {
|
||||
$breadcrumb['links'][] = [
|
||||
'title' => $catRow['name'],
|
||||
'url' => Projectproblog::getCategoryUrl($catRow['link_rewrite']),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$breadcrumb['links'][] = [
|
||||
'title' => $this->post->title,
|
||||
'url' => '',
|
||||
];
|
||||
}
|
||||
|
||||
return $breadcrumb;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Canonical URL */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* Wyłącza canonical redirection — patrz komentarz w list.php.
|
||||
*/
|
||||
protected function canonicalRedirection($canonical_url = '')
|
||||
{
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Helpery prywatne */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* Rekurencyjnie dodaje url do węzłów drzewa kategorii (sidebar).
|
||||
*/
|
||||
protected function enrichCategoryTree(array $tree, $idLang)
|
||||
{
|
||||
foreach ($tree as &$node) {
|
||||
$node['category']['url'] = Projectproblog::getCategoryUrl(
|
||||
$node['category']['link_rewrite'],
|
||||
$idLang
|
||||
);
|
||||
$node['children'] = $this->enrichCategoryTree($node['children'], $idLang);
|
||||
}
|
||||
unset($node);
|
||||
|
||||
return $tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pobiera dane kategorii po ID-kach i dodaje URL-e.
|
||||
*/
|
||||
protected function loadCategories(array $categoryIds, $idLang)
|
||||
{
|
||||
if (empty($categoryIds)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$idList = implode(',', array_map('intval', $categoryIds));
|
||||
|
||||
$rows = Db::getInstance()->executeS(
|
||||
'SELECT c.`id_category`, cl.`name`, cl.`link_rewrite`
|
||||
FROM `' . _DB_PREFIX_ . 'projectproblog_category` c
|
||||
LEFT JOIN `' . _DB_PREFIX_ . 'projectproblog_category_lang` cl
|
||||
ON c.`id_category` = cl.`id_category` AND cl.`id_lang` = ' . (int) $idLang . '
|
||||
WHERE c.`id_category` IN (' . $idList . ') AND c.`active` = 1
|
||||
ORDER BY cl.`name` ASC'
|
||||
);
|
||||
|
||||
if (!is_array($rows)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
foreach ($rows as &$row) {
|
||||
$row['url'] = Projectproblog::getCategoryUrl($row['link_rewrite'], $idLang);
|
||||
}
|
||||
unset($row);
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ustawia meta title / description / keywords dla layoutu.
|
||||
*/
|
||||
protected function setPageMeta()
|
||||
{
|
||||
if (!$this->post) {
|
||||
return;
|
||||
}
|
||||
|
||||
$metaTitle = $this->post->meta_title ?: $this->post->title;
|
||||
$metaDesc = $this->post->meta_description ?: '';
|
||||
$metaKw = $this->post->meta_keywords ?: '';
|
||||
|
||||
$page = $this->context->smarty->getTemplateVars('page') ?: [];
|
||||
|
||||
if (!isset($page['meta'])) {
|
||||
$page['meta'] = [];
|
||||
}
|
||||
|
||||
$page['meta']['title'] = $metaTitle;
|
||||
$page['meta']['description'] = $metaDesc;
|
||||
$page['meta']['keywords'] = $metaKw;
|
||||
|
||||
$this->context->smarty->assign('page', $page);
|
||||
}
|
||||
}
|
||||
8
modules/projectproblog/controllers/index.php
Normal file
8
modules/projectproblog/controllers/index.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
|
||||
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
|
||||
header("Cache-Control: no-store, no-cache, must-revalidate");
|
||||
header("Cache-Control: post-check=0, pre-check=0", false);
|
||||
header("Pragma: no-cache");
|
||||
header("Location: ../");
|
||||
exit;
|
||||
8
modules/projectproblog/index.php
Normal file
8
modules/projectproblog/index.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
|
||||
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate');
|
||||
header('Cache-Control: post-check=0, pre-check=0', false);
|
||||
header('Pragma: no-cache');
|
||||
header('Location: ../');
|
||||
exit;
|
||||
269
modules/projectproblog/projectproblog.php
Normal file
269
modules/projectproblog/projectproblog.php
Normal file
@@ -0,0 +1,269 @@
|
||||
<?php
|
||||
/**
|
||||
* Project-Pro Blog
|
||||
*
|
||||
* @author Project-Pro <https://www.project-pro.pl>
|
||||
* @copyright 2024 Project-Pro
|
||||
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
|
||||
*/
|
||||
|
||||
if (!defined('_PS_VERSION_')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/classes/BlogCategory.php';
|
||||
require_once __DIR__ . '/classes/BlogPost.php';
|
||||
|
||||
class Projectproblog extends Module
|
||||
{
|
||||
const POSTS_PER_PAGE = 9;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->name = 'projectproblog';
|
||||
$this->tab = 'content_management';
|
||||
$this->version = '1.0.0';
|
||||
$this->author = 'Project-Pro';
|
||||
$this->author_uri = 'https://www.project-pro.pl';
|
||||
$this->need_instance = 0;
|
||||
$this->bootstrap = true;
|
||||
$this->ps_versions_compliancy = [
|
||||
'min' => '1.7.0.0',
|
||||
'max' => _PS_VERSION_,
|
||||
];
|
||||
|
||||
parent::__construct();
|
||||
|
||||
$this->displayName = $this->l('Project-Pro Blog');
|
||||
$this->description = $this->l('Moduł bloga z obsługą kategorii, podkategorii i wpisów wielojęzycznych.');
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* INSTALL / UNINSTALL */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
public function install()
|
||||
{
|
||||
$this->createImgDir();
|
||||
|
||||
return parent::install()
|
||||
&& $this->installSql()
|
||||
&& $this->installTabs()
|
||||
&& $this->registerHook('moduleRoutes');
|
||||
}
|
||||
|
||||
protected function createImgDir()
|
||||
{
|
||||
$dir = _PS_MODULE_DIR_ . 'projectproblog/views/img/posts/';
|
||||
if (!is_dir($dir)) {
|
||||
@mkdir($dir, 0755, true);
|
||||
}
|
||||
}
|
||||
|
||||
public function uninstall()
|
||||
{
|
||||
return $this->uninstallTabs()
|
||||
&& $this->uninstallSql()
|
||||
&& parent::uninstall();
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* SQL */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
protected function installSql()
|
||||
{
|
||||
$sql = file_get_contents(__DIR__ . '/sql/install.sql');
|
||||
$sql = str_replace('PREFIX_', _DB_PREFIX_, $sql);
|
||||
|
||||
foreach (array_filter(array_map('trim', explode(';', $sql))) as $query) {
|
||||
if (!Db::getInstance()->execute($query)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function uninstallSql()
|
||||
{
|
||||
$sql = file_get_contents(__DIR__ . '/sql/uninstall.sql');
|
||||
$sql = str_replace('PREFIX_', _DB_PREFIX_, $sql);
|
||||
|
||||
foreach (array_filter(array_map('trim', explode(';', $sql))) as $query) {
|
||||
if (!Db::getInstance()->execute($query)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* TABS (menu admina) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
protected function installTabs()
|
||||
{
|
||||
// Szukamy odpowiedniego rodzica w menu BO — fallback do 0 (główne menu)
|
||||
$parentCandidates = ['AdminParentContent', 'AdminParentCmsContent', 'AdminParentThemes'];
|
||||
$idMenuParent = 0;
|
||||
foreach ($parentCandidates as $candidate) {
|
||||
$found = (int) Tab::getIdFromClassName($candidate);
|
||||
if ($found > 0) {
|
||||
$idMenuParent = $found;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Tab-rodzic (bez kontrolera — tylko nagłówek w menu)
|
||||
$idParent = $this->addTab(
|
||||
'AdminProjectproBlog',
|
||||
'Project-Pro Blog',
|
||||
$idMenuParent,
|
||||
''
|
||||
);
|
||||
if (!$idParent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Podmenu: Kategorie
|
||||
if (!$this->addTab(
|
||||
'AdminProjectproBlogCategories',
|
||||
'Kategorie bloga',
|
||||
$idParent,
|
||||
$this->name
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Podmenu: Wpisy
|
||||
if (!$this->addTab(
|
||||
'AdminProjectproBlogPosts',
|
||||
'Wpisy bloga',
|
||||
$idParent,
|
||||
$this->name
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function addTab($className, $name, $idParent, $module)
|
||||
{
|
||||
$tab = new Tab();
|
||||
$tab->active = 1;
|
||||
$tab->class_name = $className;
|
||||
$tab->id_parent = (int) $idParent;
|
||||
$tab->module = $module;
|
||||
$tab->name = [];
|
||||
|
||||
foreach (Language::getLanguages(false) as $lang) {
|
||||
$tab->name[$lang['id_lang']] = $name;
|
||||
}
|
||||
|
||||
if (!$tab->add()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (int) $tab->id;
|
||||
}
|
||||
|
||||
protected function uninstallTabs()
|
||||
{
|
||||
$tabs = [
|
||||
'AdminProjectproBlogCategories',
|
||||
'AdminProjectproBlogPosts',
|
||||
'AdminProjectproBlog',
|
||||
];
|
||||
|
||||
foreach ($tabs as $className) {
|
||||
$id = (int) Tab::getIdFromClassName($className);
|
||||
if ($id) {
|
||||
$tab = new Tab($id);
|
||||
$tab->delete();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* ROUTING */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
public function hookModuleRoutes()
|
||||
{
|
||||
return [
|
||||
'module-projectproblog-list' => [
|
||||
'controller' => 'list',
|
||||
'rule' => 'blog',
|
||||
'keywords' => [],
|
||||
'params' => [
|
||||
'fc' => 'module',
|
||||
'module' => $this->name,
|
||||
],
|
||||
],
|
||||
'module-projectproblog-category' => [
|
||||
'controller' => 'list',
|
||||
'rule' => 'blog/kategoria/{slug}',
|
||||
'keywords' => [
|
||||
'slug' => [
|
||||
'regexp' => '[_a-zA-Z0-9\-]+',
|
||||
'param' => 'slug',
|
||||
],
|
||||
],
|
||||
'params' => [
|
||||
'fc' => 'module',
|
||||
'module' => $this->name,
|
||||
],
|
||||
],
|
||||
'module-projectproblog-post' => [
|
||||
'controller' => 'post',
|
||||
'rule' => 'blog/wpis/{slug}',
|
||||
'keywords' => [
|
||||
'slug' => [
|
||||
'regexp' => '[_a-zA-Z0-9\-]+',
|
||||
'param' => 'slug',
|
||||
],
|
||||
],
|
||||
'params' => [
|
||||
'fc' => 'module',
|
||||
'module' => $this->name,
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* HELPERS — generowanie linków */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
public static function getBlogUrl($idLang = null)
|
||||
{
|
||||
return Context::getContext()->link->getModuleLink('projectproblog', 'list', [], true, $idLang);
|
||||
}
|
||||
|
||||
public static function getCategoryUrl($linkRewrite, $idLang = null)
|
||||
{
|
||||
return Context::getContext()->link->getModuleLink(
|
||||
'projectproblog',
|
||||
'list',
|
||||
['slug' => $linkRewrite],
|
||||
true,
|
||||
$idLang
|
||||
);
|
||||
}
|
||||
|
||||
public static function getPostUrl($linkRewrite, $idLang = null)
|
||||
{
|
||||
return Context::getContext()->link->getModuleLink(
|
||||
'projectproblog',
|
||||
'post',
|
||||
['slug' => $linkRewrite],
|
||||
true,
|
||||
$idLang
|
||||
);
|
||||
}
|
||||
}
|
||||
8
modules/projectproblog/sql/index.php
Normal file
8
modules/projectproblog/sql/index.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
|
||||
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
|
||||
header("Cache-Control: no-store, no-cache, must-revalidate");
|
||||
header("Cache-Control: post-check=0, pre-check=0", false);
|
||||
header("Pragma: no-cache");
|
||||
header("Location: ../");
|
||||
exit;
|
||||
49
modules/projectproblog/sql/install.sql
Normal file
49
modules/projectproblog/sql/install.sql
Normal file
@@ -0,0 +1,49 @@
|
||||
CREATE TABLE IF NOT EXISTS `PREFIX_projectproblog_category` (
|
||||
`id_category` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`id_parent` INT(10) UNSIGNED NOT NULL DEFAULT 0,
|
||||
`active` TINYINT(1) UNSIGNED NOT NULL DEFAULT 1,
|
||||
`position` INT(10) UNSIGNED NOT NULL DEFAULT 0,
|
||||
`date_add` DATETIME NOT NULL,
|
||||
`date_upd` DATETIME NOT NULL,
|
||||
PRIMARY KEY (`id_category`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `PREFIX_projectproblog_category_lang` (
|
||||
`id_category` INT(10) UNSIGNED NOT NULL,
|
||||
`id_lang` INT(10) UNSIGNED NOT NULL,
|
||||
`name` VARCHAR(255) NOT NULL,
|
||||
`description` TEXT,
|
||||
`link_rewrite` VARCHAR(255) NOT NULL,
|
||||
`meta_title` VARCHAR(255),
|
||||
`meta_keywords` VARCHAR(255),
|
||||
`meta_description` VARCHAR(512),
|
||||
PRIMARY KEY (`id_category`, `id_lang`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `PREFIX_projectproblog_post` (
|
||||
`id_post` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`active` TINYINT(1) UNSIGNED NOT NULL DEFAULT 1,
|
||||
`thumbnail` VARCHAR(255) DEFAULT NULL,
|
||||
`date_add` DATETIME NOT NULL,
|
||||
`date_upd` DATETIME NOT NULL,
|
||||
PRIMARY KEY (`id_post`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `PREFIX_projectproblog_post_lang` (
|
||||
`id_post` INT(10) UNSIGNED NOT NULL,
|
||||
`id_lang` INT(10) UNSIGNED NOT NULL,
|
||||
`title` VARCHAR(255) NOT NULL,
|
||||
`intro` TEXT,
|
||||
`content` LONGTEXT,
|
||||
`link_rewrite` VARCHAR(255) NOT NULL,
|
||||
`meta_title` VARCHAR(255),
|
||||
`meta_keywords` VARCHAR(255),
|
||||
`meta_description` VARCHAR(512),
|
||||
PRIMARY KEY (`id_post`, `id_lang`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `PREFIX_projectproblog_post_category` (
|
||||
`id_post` INT(10) UNSIGNED NOT NULL,
|
||||
`id_category` INT(10) UNSIGNED NOT NULL,
|
||||
PRIMARY KEY (`id_post`, `id_category`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
5
modules/projectproblog/sql/uninstall.sql
Normal file
5
modules/projectproblog/sql/uninstall.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
DROP TABLE IF EXISTS `PREFIX_projectproblog_post_category`;
|
||||
DROP TABLE IF EXISTS `PREFIX_projectproblog_post_lang`;
|
||||
DROP TABLE IF EXISTS `PREFIX_projectproblog_post`;
|
||||
DROP TABLE IF EXISTS `PREFIX_projectproblog_category_lang`;
|
||||
DROP TABLE IF EXISTS `PREFIX_projectproblog_category`;
|
||||
532
modules/projectproblog/views/css/blog.css
Normal file
532
modules/projectproblog/views/css/blog.css
Normal file
@@ -0,0 +1,532 @@
|
||||
/* =========================================================
|
||||
Project-Pro Blog — style frontu
|
||||
========================================================= */
|
||||
|
||||
:root {
|
||||
--blog-accent: #3a6fd8;
|
||||
--blog-accent-dk: #2a55b0;
|
||||
--blog-accent-lt: #e8eef9;
|
||||
--blog-text: #2c2c2c;
|
||||
--blog-muted: #777;
|
||||
--blog-border: #e2e6ea;
|
||||
--blog-bg: #f7f8fc;
|
||||
--blog-white: #ffffff;
|
||||
--blog-radius: 8px;
|
||||
--blog-shadow: 0 2px 10px rgba(0,0,0,.07);
|
||||
--blog-shadow-hov: 0 6px 24px rgba(0,0,0,.13);
|
||||
}
|
||||
|
||||
/* ---- Ogólny layout ---------------------------------------- */
|
||||
#blog-wrap {
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
/* ---- Sidebar ----------------------------------------------- */
|
||||
#blog-sidebar {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
#blog-sidebar {
|
||||
position: sticky;
|
||||
top: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.blog-cats-widget {
|
||||
background: var(--blog-white);
|
||||
border: 1px solid var(--blog-border);
|
||||
border-radius: var(--blog-radius);
|
||||
padding: 1.25rem 1.5rem;
|
||||
box-shadow: var(--blog-shadow);
|
||||
}
|
||||
|
||||
.blog-cats-widget h4 {
|
||||
font-size: .78rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .1em;
|
||||
color: var(--blog-muted);
|
||||
margin: 0 0 .9rem;
|
||||
padding-bottom: .65rem;
|
||||
border-bottom: 2px solid var(--blog-border);
|
||||
}
|
||||
|
||||
.blog-cat-link {
|
||||
display: block;
|
||||
margin-bottom: .6rem;
|
||||
font-size: .88rem;
|
||||
font-weight: 600;
|
||||
color: var(--blog-accent);
|
||||
text-decoration: none;
|
||||
padding: .3rem .5rem;
|
||||
border-radius: 5px;
|
||||
transition: background .15s;
|
||||
}
|
||||
|
||||
.blog-cat-link:hover,
|
||||
.blog-cat-link.active {
|
||||
background: var(--blog-accent-lt);
|
||||
color: var(--blog-accent-dk);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Drzewo kategorii */
|
||||
.blog-cat-tree,
|
||||
.blog-cat-tree ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.blog-cat-tree ul {
|
||||
padding-left: .9rem;
|
||||
margin-top: .15rem;
|
||||
border-left: 2px solid var(--blog-border);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.blog-cat-tree ul.open {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.blog-cat-tree li {
|
||||
margin: .1rem 0;
|
||||
}
|
||||
|
||||
.blog-cat-tree a {
|
||||
display: block;
|
||||
color: var(--blog-text);
|
||||
text-decoration: none;
|
||||
font-size: .87rem;
|
||||
padding: .3rem .45rem;
|
||||
border-radius: 5px;
|
||||
transition: background .15s, color .15s;
|
||||
}
|
||||
|
||||
.blog-cat-tree a:hover,
|
||||
.blog-cat-tree a.active {
|
||||
background: var(--blog-accent-lt);
|
||||
color: var(--blog-accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.blog-cat-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
color: var(--blog-muted);
|
||||
font-size: .7rem;
|
||||
vertical-align: middle;
|
||||
user-select: none;
|
||||
transition: color .15s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.blog-cat-toggle:hover {
|
||||
color: var(--blog-accent);
|
||||
}
|
||||
|
||||
/* ---- Nagłówek listy --------------------------------------- */
|
||||
.blog-list-title {
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
color: var(--blog-text);
|
||||
margin: 0 0 1.5rem;
|
||||
padding-bottom: .6rem;
|
||||
border-bottom: 3px solid var(--blog-accent);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* ---- Siatka kart ------------------------------------------ */
|
||||
.blog-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 -.75rem;
|
||||
}
|
||||
|
||||
.blog-grid-item {
|
||||
width: 50%;
|
||||
padding: 0 .75rem;
|
||||
margin-bottom: 1.75rem;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@media (max-width: 575px) {
|
||||
.blog-grid-item {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Karta wpisu */
|
||||
.blog-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid var(--blog-border);
|
||||
border-radius: var(--blog-radius);
|
||||
overflow: hidden;
|
||||
background: var(--blog-white);
|
||||
width: 100%;
|
||||
transition: box-shadow .25s, transform .25s;
|
||||
}
|
||||
|
||||
.blog-card:hover {
|
||||
box-shadow: var(--blog-shadow-hov);
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
.blog-card__thumb {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
aspect-ratio: 16/9;
|
||||
background: var(--blog-bg);
|
||||
}
|
||||
|
||||
.blog-card__thumb img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform .4s ease;
|
||||
}
|
||||
|
||||
.blog-card:hover .blog-card__thumb img {
|
||||
transform: scale(1.06);
|
||||
}
|
||||
|
||||
.blog-card__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
padding: 1.1rem 1.2rem;
|
||||
}
|
||||
|
||||
.blog-card__title {
|
||||
font-size: .97rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 .5rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.blog-card__title a {
|
||||
color: var(--blog-text);
|
||||
text-decoration: none;
|
||||
transition: color .15s;
|
||||
}
|
||||
|
||||
.blog-card__title a:hover {
|
||||
color: var(--blog-accent);
|
||||
}
|
||||
|
||||
.blog-card__intro {
|
||||
font-size: .85rem;
|
||||
color: var(--blog-muted);
|
||||
line-height: 1.55;
|
||||
flex: 1;
|
||||
margin-bottom: .85rem;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.blog-card__footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: auto;
|
||||
padding-top: .75rem;
|
||||
border-top: 1px solid var(--blog-border);
|
||||
}
|
||||
|
||||
.blog-card__date {
|
||||
font-size: .78rem;
|
||||
color: var(--blog-muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .3rem;
|
||||
}
|
||||
|
||||
.blog-card__date::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: .55rem;
|
||||
height: .55rem;
|
||||
border-radius: 50%;
|
||||
background: var(--blog-border);
|
||||
}
|
||||
|
||||
.blog-card__more {
|
||||
font-size: .78rem;
|
||||
font-weight: 700;
|
||||
color: var(--blog-accent);
|
||||
text-decoration: none;
|
||||
background: var(--blog-accent-lt);
|
||||
border-radius: 20px;
|
||||
padding: .25rem .75rem;
|
||||
transition: background .15s, color .15s;
|
||||
letter-spacing: .02em;
|
||||
}
|
||||
|
||||
.blog-card__more:hover {
|
||||
background: var(--blog-accent);
|
||||
color: var(--blog-white);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.blog-empty {
|
||||
padding: 2.5rem;
|
||||
text-align: center;
|
||||
color: var(--blog-muted);
|
||||
border: 2px dashed var(--blog-border);
|
||||
border-radius: var(--blog-radius);
|
||||
font-size: .95rem;
|
||||
}
|
||||
|
||||
/* ---- Paginacja -------------------------------------------- */
|
||||
.blog-pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: .3rem;
|
||||
margin-top: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.blog-pagination a,
|
||||
.blog-pagination span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 2.2rem;
|
||||
height: 2.2rem;
|
||||
padding: 0 .5rem;
|
||||
border: 1px solid var(--blog-border);
|
||||
border-radius: 6px;
|
||||
font-size: .88rem;
|
||||
text-decoration: none;
|
||||
color: var(--blog-text);
|
||||
transition: background .15s, border-color .15s, color .15s;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.blog-pagination a:hover {
|
||||
background: var(--blog-accent-lt);
|
||||
border-color: var(--blog-accent);
|
||||
color: var(--blog-accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.blog-pagination .current {
|
||||
background: var(--blog-accent);
|
||||
border-color: var(--blog-accent);
|
||||
color: var(--blog-white);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.blog-pagination .disabled {
|
||||
color: #ccc;
|
||||
border-color: #eee;
|
||||
pointer-events: none;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
/* ---- Szczegół wpisu --------------------------------------- */
|
||||
.blog-post {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.blog-post-header {
|
||||
margin-bottom: 1.75rem;
|
||||
padding-bottom: 1.25rem;
|
||||
border-bottom: 1px solid var(--blog-border);
|
||||
}
|
||||
|
||||
.blog-post-title {
|
||||
font-size: 2rem;
|
||||
font-weight: 800;
|
||||
line-height: 1.25;
|
||||
color: var(--blog-text);
|
||||
margin: 0 0 1rem;
|
||||
letter-spacing: -.02em;
|
||||
}
|
||||
|
||||
@media (max-width: 575px) {
|
||||
.blog-post-title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.blog-post-meta {
|
||||
font-size: .83rem;
|
||||
color: var(--blog-muted);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: .6rem 1.2rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.blog-post-meta time {
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.blog-post-meta a {
|
||||
color: var(--blog-accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.blog-post-meta a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Kategorie jako badges */
|
||||
.blog-post-cats {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: .3rem;
|
||||
}
|
||||
|
||||
.cat-badge {
|
||||
display: inline-block;
|
||||
background: var(--blog-accent-lt);
|
||||
border: 1px solid #c5d5f0;
|
||||
border-radius: 20px;
|
||||
font-size: .75rem;
|
||||
font-weight: 600;
|
||||
padding: .2rem .65rem;
|
||||
color: var(--blog-accent);
|
||||
text-decoration: none;
|
||||
letter-spacing: .02em;
|
||||
transition: background .15s, color .15s;
|
||||
}
|
||||
|
||||
.cat-badge:hover {
|
||||
background: var(--blog-accent);
|
||||
color: var(--blog-white);
|
||||
border-color: var(--blog-accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Miniaturka */
|
||||
.blog-post-thumb {
|
||||
margin-bottom: 1.75rem;
|
||||
border-radius: var(--blog-radius);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--blog-shadow);
|
||||
}
|
||||
|
||||
.blog-post-thumb img {
|
||||
width: 100%;
|
||||
max-height: 460px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Wstęp (intro) */
|
||||
.blog-post-intro {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 400;
|
||||
color: #3a3a3a;
|
||||
line-height: 1.7;
|
||||
padding: 1rem 1.25rem 1rem 1.5rem;
|
||||
background: var(--blog-bg);
|
||||
border-left: 4px solid var(--blog-accent);
|
||||
border-radius: 0 var(--blog-radius) var(--blog-radius) 0;
|
||||
margin-bottom: 2rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Treść wpisu */
|
||||
.blog-post-content {
|
||||
font-size: .97rem;
|
||||
line-height: 1.8;
|
||||
color: var(--blog-text);
|
||||
}
|
||||
|
||||
.blog-post-content h2 {
|
||||
font-size: 1.45rem;
|
||||
font-weight: 700;
|
||||
margin: 2rem 0 .75rem;
|
||||
color: var(--blog-text);
|
||||
padding-bottom: .4rem;
|
||||
border-bottom: 2px solid var(--blog-border);
|
||||
}
|
||||
|
||||
.blog-post-content h3 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
margin: 1.5rem 0 .6rem;
|
||||
color: var(--blog-text);
|
||||
}
|
||||
|
||||
.blog-post-content h4 {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
margin: 1.2rem 0 .5rem;
|
||||
}
|
||||
|
||||
.blog-post-content p {
|
||||
margin-bottom: 1.1rem;
|
||||
}
|
||||
|
||||
.blog-post-content ul,
|
||||
.blog-post-content ol {
|
||||
margin-bottom: 1.1rem;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.blog-post-content li {
|
||||
margin-bottom: .35rem;
|
||||
}
|
||||
|
||||
.blog-post-content img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 6px;
|
||||
margin: .5rem 0;
|
||||
}
|
||||
|
||||
.blog-post-content a {
|
||||
color: var(--blog-accent);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.blog-post-content a:hover {
|
||||
color: var(--blog-accent-dk);
|
||||
}
|
||||
|
||||
.blog-post-content blockquote {
|
||||
margin: 1.5rem 0;
|
||||
padding: .75rem 1.25rem;
|
||||
border-left: 4px solid var(--blog-accent);
|
||||
background: var(--blog-bg);
|
||||
border-radius: 0 6px 6px 0;
|
||||
font-style: italic;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
/* Powrót */
|
||||
.blog-post-back {
|
||||
margin-top: 2.5rem;
|
||||
padding: 1rem 1.25rem;
|
||||
background: var(--blog-bg);
|
||||
border-radius: var(--blog-radius);
|
||||
border: 1px solid var(--blog-border);
|
||||
font-size: .88rem;
|
||||
}
|
||||
|
||||
.blog-post-back a {
|
||||
color: var(--blog-accent);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: color .15s;
|
||||
}
|
||||
|
||||
.blog-post-back a:hover {
|
||||
color: var(--blog-accent-dk);
|
||||
text-decoration: underline;
|
||||
}
|
||||
1
modules/projectproblog/views/css/index.php
Normal file
1
modules/projectproblog/views/css/index.php
Normal file
@@ -0,0 +1 @@
|
||||
<?php header('Location: ../'); exit;
|
||||
1
modules/projectproblog/views/img/index.php
Normal file
1
modules/projectproblog/views/img/index.php
Normal file
@@ -0,0 +1 @@
|
||||
<?php header('Location: ../'); exit;
|
||||
8
modules/projectproblog/views/img/posts/index.php
Normal file
8
modules/projectproblog/views/img/posts/index.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
|
||||
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate');
|
||||
header('Cache-Control: post-check=0, pre-check=0', false);
|
||||
header('Pragma: no-cache');
|
||||
header('Location: ../');
|
||||
exit;
|
||||
8
modules/projectproblog/views/index.php
Normal file
8
modules/projectproblog/views/index.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
|
||||
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
|
||||
header("Cache-Control: no-store, no-cache, must-revalidate");
|
||||
header("Cache-Control: post-check=0, pre-check=0", false);
|
||||
header("Pragma: no-cache");
|
||||
header("Location: ../");
|
||||
exit;
|
||||
8
modules/projectproblog/views/templates/admin/index.php
Normal file
8
modules/projectproblog/views/templates/admin/index.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
|
||||
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
|
||||
header("Cache-Control: no-store, no-cache, must-revalidate");
|
||||
header("Cache-Control: post-check=0, pre-check=0", false);
|
||||
header("Pragma: no-cache");
|
||||
header("Location: ../");
|
||||
exit;
|
||||
@@ -0,0 +1,53 @@
|
||||
{**
|
||||
* Project-Pro Blog — partial: drzewo kategorii (rekurencyjne)
|
||||
*
|
||||
* Parametry:
|
||||
* $tree — wynik BlogCategory::getCategoryTree() z URL-ami
|
||||
* $current_cat — aktualny obiekt BlogCategory lub null
|
||||
*}
|
||||
|
||||
{if $tree}
|
||||
<ul class="blog-cat-tree">
|
||||
{foreach from=$tree item=node}
|
||||
{assign var="cat" value=$node.category}
|
||||
{assign var="children" value=$node.children}
|
||||
{assign var="isActive" value=($current_cat && $current_cat->id == $cat.id_category)}
|
||||
{assign var="hasActive" value=false}
|
||||
|
||||
{* Sprawdź czy aktywna kategoria jest wśród dzieci (do automatycznego rozwinięcia) *}
|
||||
{if $current_cat && $children}
|
||||
{foreach from=$children item=child}
|
||||
{if $child.category.id_category == $current_cat->id}
|
||||
{assign var="hasActive" value=true}
|
||||
{/if}
|
||||
{/foreach}
|
||||
{/if}
|
||||
|
||||
<li class="blog-cat-item{if $isActive} is-active{/if}{if $children} has-children{/if}">
|
||||
|
||||
{if $children}
|
||||
<span class="blog-cat-toggle" data-target="#cat-sub-{$cat.id_category}"
|
||||
title="{if $isActive || $hasActive}{l s='Zwiń' mod='projectproblog'}{else}{l s='Rozwiń' mod='projectproblog'}{/if}">
|
||||
{if $isActive || $hasActive}▼{else}►{/if}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<a href="{$cat.url|escape:'html':'UTF-8'}"
|
||||
class="blog-cat-link{if $isActive} active{/if}">
|
||||
{$cat.name|escape:'html':'UTF-8'}
|
||||
</a>
|
||||
|
||||
{if $children}
|
||||
<ul id="cat-sub-{$cat.id_category}"
|
||||
class="{if $isActive || $hasActive}open{/if}">
|
||||
{include
|
||||
file='module:projectproblog/views/templates/front/_category-tree.tpl'
|
||||
tree=$children
|
||||
current_cat=$current_cat}
|
||||
</ul>
|
||||
{/if}
|
||||
|
||||
</li>
|
||||
{/foreach}
|
||||
</ul>
|
||||
{/if}
|
||||
8
modules/projectproblog/views/templates/front/index.php
Normal file
8
modules/projectproblog/views/templates/front/index.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
|
||||
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
|
||||
header("Cache-Control: no-store, no-cache, must-revalidate");
|
||||
header("Cache-Control: post-check=0, pre-check=0", false);
|
||||
header("Pragma: no-cache");
|
||||
header("Location: ../");
|
||||
exit;
|
||||
140
modules/projectproblog/views/templates/front/list.tpl
Normal file
140
modules/projectproblog/views/templates/front/list.tpl
Normal file
@@ -0,0 +1,140 @@
|
||||
{**
|
||||
* Project-Pro Blog — strona listy wpisów
|
||||
*}
|
||||
{extends file='layouts/layout-full-width.tpl'}
|
||||
|
||||
{block name='content'}
|
||||
<section id="blog-wrap" class="container">
|
||||
<div class="row">
|
||||
|
||||
{* ====================================================
|
||||
LEWA KOLUMNA — drzewo kategorii
|
||||
==================================================== *}
|
||||
<aside id="blog-sidebar" class="col-md-3">
|
||||
<div class="blog-cats-widget">
|
||||
<h4>{l s='Kategorie' mod='projectproblog'}</h4>
|
||||
|
||||
<a href="{$blog_url|escape:'html':'UTF-8'}"
|
||||
class="blog-cat-link{if !$blog_current_cat} active{/if}"
|
||||
style="display:block;margin-bottom:.5rem;">
|
||||
{l s='Wszystkie wpisy' mod='projectproblog'}
|
||||
</a>
|
||||
|
||||
{include
|
||||
file='module:projectproblog/views/templates/front/_category-tree.tpl'
|
||||
tree=$blog_category_tree
|
||||
current_cat=$blog_current_cat}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{* ====================================================
|
||||
PRAWA KOLUMNA — lista wpisów + paginacja
|
||||
==================================================== *}
|
||||
<div id="blog-content" class="col-md-9">
|
||||
|
||||
<h1 class="blog-list-title">
|
||||
{if $blog_current_cat}
|
||||
{$blog_current_cat->name|escape:'html':'UTF-8'}
|
||||
{else}
|
||||
{l s='Blog' mod='projectproblog'}
|
||||
{/if}
|
||||
</h1>
|
||||
|
||||
{if $blog_posts}
|
||||
|
||||
<div class="blog-grid">
|
||||
{foreach from=$blog_posts item=post}
|
||||
<div class="blog-grid-item">
|
||||
<article class="blog-card">
|
||||
|
||||
{if $post.thumbnail_url}
|
||||
<a href="{$post.url|escape:'html':'UTF-8'}" class="blog-card__thumb">
|
||||
<img src="{$post.thumbnail_url|escape:'html':'UTF-8'}"
|
||||
alt="{$post.title|escape:'html':'UTF-8'}"
|
||||
loading="lazy">
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
<div class="blog-card__body">
|
||||
<h2 class="blog-card__title">
|
||||
<a href="{$post.url|escape:'html':'UTF-8'}">
|
||||
{$post.title|escape:'html':'UTF-8'}
|
||||
</a>
|
||||
</h2>
|
||||
|
||||
{if $post.intro}
|
||||
<div class="blog-card__intro">
|
||||
{$post.intro|strip_tags|truncate:180:'…'}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="blog-card__footer">
|
||||
<span class="blog-card__date">
|
||||
{$post.date_add|date_format:'%d.%m.%Y'}
|
||||
</span>
|
||||
<a href="{$post.url|escape:'html':'UTF-8'}" class="blog-card__more">
|
||||
{l s='Czytaj więcej' mod='projectproblog'} ›
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</article>
|
||||
</div>
|
||||
{/foreach}
|
||||
</div>
|
||||
|
||||
{* ---- Paginacja ---- *}
|
||||
{if $blog_pagination && $blog_pages_count > 1}
|
||||
<nav class="blog-pagination" aria-label="{l s='Strony' mod='projectproblog'}">
|
||||
|
||||
{if $blog_pagination.prev}
|
||||
<a href="{$blog_pagination.prev|escape:'html':'UTF-8'}" aria-label="{l s='Poprzednia' mod='projectproblog'}">«</a>
|
||||
{else}
|
||||
<span class="disabled">«</span>
|
||||
{/if}
|
||||
|
||||
{foreach from=$blog_pagination.pages item=pg}
|
||||
{if $pg.current}
|
||||
<span class="current">{$pg.page}</span>
|
||||
{else}
|
||||
<a href="{$pg.url|escape:'html':'UTF-8'}">{$pg.page}</a>
|
||||
{/if}
|
||||
{/foreach}
|
||||
|
||||
{if $blog_pagination.next}
|
||||
<a href="{$blog_pagination.next|escape:'html':'UTF-8'}" aria-label="{l s='Następna' mod='projectproblog'}">»</a>
|
||||
{else}
|
||||
<span class="disabled">»</span>
|
||||
{/if}
|
||||
|
||||
</nav>
|
||||
{/if}
|
||||
|
||||
{else}
|
||||
<p class="blog-empty">
|
||||
{l s='Brak wpisów do wyświetlenia.' mod='projectproblog'}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
</div>{* /blog-content *}
|
||||
</div>{* /row *}
|
||||
</section>
|
||||
{/block}
|
||||
|
||||
{block name='javascript_bottom' append}
|
||||
<script>
|
||||
(function () {
|
||||
'use strict';
|
||||
/* Rozwijanie/zwijanie podkategorii */
|
||||
document.querySelectorAll('.blog-cat-toggle').forEach(function (toggle) {
|
||||
toggle.addEventListener('click', function () {
|
||||
var targetId = this.getAttribute('data-target');
|
||||
var sub = document.querySelector(targetId);
|
||||
if (!sub) { return; }
|
||||
var isOpen = sub.classList.toggle('open');
|
||||
this.innerHTML = isOpen ? '▼' : '►';
|
||||
});
|
||||
});
|
||||
}());
|
||||
</script>
|
||||
{/block}
|
||||
134
modules/projectproblog/views/templates/front/post.tpl
Normal file
134
modules/projectproblog/views/templates/front/post.tpl
Normal file
@@ -0,0 +1,134 @@
|
||||
{**
|
||||
* Project-Pro Blog — strona szczegółu wpisu
|
||||
*}
|
||||
{extends file='layouts/layout-full-width.tpl'}
|
||||
|
||||
{block name='content'}
|
||||
<section id="blog-wrap" class="container">
|
||||
<div class="row">
|
||||
|
||||
{* ====================================================
|
||||
LEWA KOLUMNA — drzewo kategorii
|
||||
==================================================== *}
|
||||
<aside id="blog-sidebar" class="col-md-3">
|
||||
<div class="blog-cats-widget">
|
||||
<h4>{l s='Kategorie' mod='projectproblog'}</h4>
|
||||
|
||||
<a href="{$blog_url|escape:'html':'UTF-8'}"
|
||||
class="blog-cat-link"
|
||||
style="display:block;margin-bottom:.5rem;">
|
||||
{l s='Wszystkie wpisy' mod='projectproblog'}
|
||||
</a>
|
||||
|
||||
{include
|
||||
file='module:projectproblog/views/templates/front/_category-tree.tpl'
|
||||
tree=$blog_category_tree
|
||||
current_cat=$blog_current_cat}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{* ====================================================
|
||||
PRAWA KOLUMNA — treść wpisu
|
||||
==================================================== *}
|
||||
<div id="blog-content" class="col-md-9">
|
||||
|
||||
<article class="blog-post" itemscope itemtype="https://schema.org/BlogPosting">
|
||||
|
||||
{* ---- Nagłówek ---- *}
|
||||
<header class="blog-post-header">
|
||||
|
||||
<h1 class="blog-post-title" itemprop="headline">
|
||||
{$blog_post->title|escape:'html':'UTF-8'}
|
||||
</h1>
|
||||
|
||||
<div class="blog-post-meta">
|
||||
<span>
|
||||
<time datetime="{$blog_post->date_add|date_format:'%Y-%m-%d'}" itemprop="datePublished">
|
||||
{$blog_post->date_add|date_format:'%d.%m.%Y'}
|
||||
</time>
|
||||
</span>
|
||||
|
||||
{if $blog_show_updated}
|
||||
<span>
|
||||
{l s='Aktualizacja:' mod='projectproblog'}
|
||||
<time datetime="{$blog_post->date_upd|date_format:'%Y-%m-%d'}" itemprop="dateModified">
|
||||
{$blog_post->date_upd|date_format:'%d.%m.%Y'}
|
||||
</time>
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{if $blog_post_cats}
|
||||
<span class="blog-post-cats">
|
||||
{foreach from=$blog_post_cats item=cat}
|
||||
<a href="{$cat.url|escape:'html':'UTF-8'}"
|
||||
class="cat-badge"
|
||||
itemprop="articleSection">
|
||||
{$cat.name|escape:'html':'UTF-8'}
|
||||
</a>
|
||||
{/foreach}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
</header>
|
||||
|
||||
{* ---- Miniaturka ---- *}
|
||||
{if $blog_thumbnail}
|
||||
<figure class="blog-post-thumb">
|
||||
<img src="{$blog_thumbnail|escape:'html':'UTF-8'}"
|
||||
alt="{$blog_post->title|escape:'html':'UTF-8'}"
|
||||
itemprop="image">
|
||||
</figure>
|
||||
{/if}
|
||||
|
||||
{* ---- Wstęp ---- *}
|
||||
{if $blog_post->intro}
|
||||
<div class="blog-post-intro" itemprop="description">
|
||||
{$blog_post->intro nofilter}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{* ---- Treść ---- *}
|
||||
{if $blog_post->content}
|
||||
<div class="blog-post-content" itemprop="articleBody">
|
||||
{$blog_post->content nofilter}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{* ---- Powrót do listy / kategorii ---- *}
|
||||
<div class="blog-post-back">
|
||||
{if $blog_primary_cat}
|
||||
<a href="{$blog_primary_cat.url|escape:'html':'UTF-8'}">
|
||||
← {l s='Wróć do kategorii' mod='projectproblog'}:
|
||||
{$blog_primary_cat.name|escape:'html':'UTF-8'}
|
||||
</a>
|
||||
·
|
||||
{/if}
|
||||
<a href="{$blog_url|escape:'html':'UTF-8'}">
|
||||
{l s='Wszystkie wpisy' mod='projectproblog'}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</article>
|
||||
|
||||
</div>{* /blog-content *}
|
||||
</div>{* /row *}
|
||||
</section>
|
||||
{/block}
|
||||
|
||||
{block name='javascript_bottom' append}
|
||||
<script>
|
||||
(function () {
|
||||
'use strict';
|
||||
document.querySelectorAll('.blog-cat-toggle').forEach(function (toggle) {
|
||||
toggle.addEventListener('click', function () {
|
||||
var targetId = this.getAttribute('data-target');
|
||||
var sub = document.querySelector(targetId);
|
||||
if (!sub) { return; }
|
||||
var isOpen = sub.classList.toggle('open');
|
||||
this.innerHTML = isOpen ? '▼' : '►';
|
||||
});
|
||||
});
|
||||
}());
|
||||
</script>
|
||||
{/block}
|
||||
8
modules/projectproblog/views/templates/index.php
Normal file
8
modules/projectproblog/views/templates/index.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
|
||||
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
|
||||
header("Cache-Control: no-store, no-cache, must-revalidate");
|
||||
header("Cache-Control: post-check=0, pre-check=0", false);
|
||||
header("Pragma: no-cache");
|
||||
header("Location: ../");
|
||||
exit;
|
||||
Reference in New Issue
Block a user