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:
2026-03-03 15:24:51 +01:00
parent 8d14e5d95c
commit 5f93428041
35 changed files with 3128 additions and 2 deletions

124
.htaccess Normal file
View 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
View 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
View File

@@ -0,0 +1 @@
/cache

126
.serena/project.yml Normal file
View 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
View File

@@ -12,6 +12,9 @@
"ignoreRemoteModification": true, "ignoreRemoteModification": true,
"ignore": [ "ignore": [
".git", ".git",
"/.vscode" "/.vscode",
"/.claude",
"/.serena",
"CLAUDE.md"
] ]
} }

69
CLAUDE.md Normal file
View 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

View File

@@ -26,7 +26,11 @@
/* Debug only */ /* Debug only */
if (!defined('_PS_MODE_DEV_')) { if (!defined('_PS_MODE_DEV_')) {
define('_PS_MODE_DEV_', false); if ( $_SERVER['REMOTE_ADDR'] === '91.189.216.43' ) {
define('_PS_MODE_DEV_', true);
} else {
define('_PS_MODE_DEV_', false);
}
} }
/* Compatibility warning */ /* Compatibility warning */
define('_PS_DISPLAY_COMPATIBILITY_WARNING_', false); define('_PS_DISPLAY_COMPATIBILITY_WARNING_', false);

10
memory/MEMORY.md Normal file
View 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

View 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

View 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);
}
}

View 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;
}
}

View 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;

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View 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;

View 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;

View 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);
}
}

View 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);
}
}

View 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;

View 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;

View 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
);
}
}

View 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;

View 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;

View 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`;

View 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;
}

View File

@@ -0,0 +1 @@
<?php header('Location: ../'); exit;

View File

@@ -0,0 +1 @@
<?php header('Location: ../'); exit;

View 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;

View 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;

View 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;

View File

@@ -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}&#9660;{else}&#9658;{/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}

View 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;

View 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'} &rsaquo;
</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'}">&laquo;</a>
{else}
<span class="disabled">&laquo;</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'}">&raquo;</a>
{else}
<span class="disabled">&raquo;</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 ? '&#9660;' : '&#9658;';
});
});
}());
</script>
{/block}

View 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'}">
&larr; {l s='Wróć do kategorii' mod='projectproblog'}:
{$blog_primary_cat.name|escape:'html':'UTF-8'}
</a>
&nbsp;&middot;&nbsp;
{/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 ? '&#9660;' : '&#9658;';
});
});
}());
</script>
{/block}

View 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;