diff options
| author | George Halkiadakis <gchalkiadakis@sklavenitis.co.gr> | 2023-03-12 07:16:23 +0200 |
|---|---|---|
| committer | George Halkiadakis <gchalkiadakis@sklavenitis.co.gr> | 2023-03-12 07:16:23 +0200 |
| commit | 7076343338ae3439f3c86f01144818abe8c31978 (patch) | |
| tree | 794abb1e6b8f821091fd095341885496b6dad51d /html/app | |
| parent | 47cbb529f5723b246125ae083a193e11481b89ef (diff) | |
| download | classroom-7076343338ae3439f3c86f01144818abe8c31978.tar.gz classroom-7076343338ae3439f3c86f01144818abe8c31978.tar.bz2 classroom-7076343338ae3439f3c86f01144818abe8c31978.zip | |
add container helpers; constuct public directory-tree
Diffstat (limited to 'html/app')
| -rw-r--r-- | html/app/config/assets.php | 43 | ||||
| -rw-r--r-- | html/app/controllers/Product.php | 52 | ||||
| -rw-r--r-- | html/app/controllers/Resolve.php | 91 | ||||
| -rw-r--r-- | html/app/controllers/admin/.gitkeep | 0 | ||||
| -rw-r--r-- | html/app/controllers/api/.gitkeep | 0 | ||||
| -rw-r--r-- | html/app/controllers/api/Common_api.php | 281 | ||||
| -rw-r--r-- | html/app/controllers/api/Doc_api.php | 34 | ||||
| -rw-r--r-- | html/app/controllers/cli/CliContoller.php | 85 | ||||
| -rw-r--r-- | html/app/models/Jorge.php | 169 | ||||
| -rw-r--r-- | html/app/models/_info.md | 84 | ||||
| -rw-r--r-- | html/app/models/cms/Page_model.php | 18 | ||||
| -rw-r--r-- | html/app/models/market/Market_repository.php | 235 | ||||
| -rw-r--r-- | html/app/models/market/ProductCategories_model.php | 199 | ||||
| -rw-r--r-- | html/app/models/market/Product_model.php | 49 | ||||
| -rw-r--r-- | html/app/models/todo.md | 132 | ||||
| -rw-r--r-- | html/app/routes/api.php | 64 | ||||
| -rw-r--r-- | html/app/routes/backend.php | 9 | ||||
| -rw-r--r-- | html/app/routes/frontend.php | 135 | ||||
| -rw-r--r-- | html/app/views/basic.php | 53 | ||||
| -rw-r--r-- | html/app/views/group.php | 18 | ||||
| -rw-r--r-- | html/app/views/item.php | 4 | ||||
| -rw-r--r-- | html/app/views/products_list.php | 47 |
22 files changed, 1802 insertions, 0 deletions
diff --git a/html/app/config/assets.php b/html/app/config/assets.php new file mode 100644 index 0000000..9203c47 --- /dev/null +++ b/html/app/config/assets.php @@ -0,0 +1,43 @@ +<?php + +// PATHS USED HEAVILY ////////////////////////////////////////////////////////// +// ----------------------------------------------------------------------------- +// these paths are used in order to easily find and autoload useful elemets + + +// JAVASCRIPT LIBRARY PATHS +// ----------------------------------------------------------------------------- +define('JS_DIR', '/content/'); // <base> path to contruct link +define('JS_LIBRARIES', array( // path after <base> to the actual file + 'jquery' => 'lib/jquery/dist/jquery.min.js', + + 'require/files' => 'lib/require/require.files.js', + 'require/slim' => 'lib/require/require.slim.js', + + 'plugins' => 'lib/plugins/plugins.min.js', + 'plugins/setup' => 'lib/plugins/plugins.setup.js' + ) +); + + +// CSS ASSET PATHS +// ----------------------------------------------------------------------------- +define('CSS_DIR', '/content/css/'); // <base> path to contruct link +define('CSS_FILES', array( // path after <base> to the actual file + 'main' => 'main.min.css', + 'filter' => 'filter.css', + 'overides' => 'overides.css', + ) +); + + +// FONTS +// ----------------------------------------------------------------------------- +define('FONTS_DIR', '/content/fonts/'); +define('FONT_FILES', array( + 'iconfont' => 'iconfont.woff2', + 'CFAstyStdBold' => 'CFAstyStd-Bold.woff2', + 'CFAstyStdBook' => 'CFAstyStd-Book.woff2', + 'CFAstyStdMedium' => 'CFAstyStd-Medium.woff2' + ) +); diff --git a/html/app/controllers/Product.php b/html/app/controllers/Product.php new file mode 100644 index 0000000..0193ed9 --- /dev/null +++ b/html/app/controllers/Product.php @@ -0,0 +1,52 @@ +<?php + +namespace app\controllers; + +use app\models\market\Product_model; +use app\models\market\ProductCategories_model; + +class Product { + + public static function fromUrl($url, $pathArr) + { + $product = proxy([\app\models\market\Product_model::class, 'fromUrl'], + [ $url ], CACHE_PRODUCT_TTL + ); + + // TODO: + // verify path, else redirect + + if ($product === false) { + header("HTTP/1.0 404 Not Found"); + render_view('error/404', ['message' => 'Of all the things I\'ve lost, I miss my mind the most.']); + return ; + } + + // render page + load_template('products', [ 'data' => [ + 'title' => $product['Title'], + 'seo' => [], + 'tree' => proxy( + [\app\models\market\ProductCategories_model::class, 'tree'], + [], CACHE_ROOT_TTL + ), + 'hierarchyPath' => explode('.', substr($product['Hierarchy'], 1, -1)), + 'sections' => [ + [ + 'view' => 'content/product_list/Product_List_Filters', + 'key' => 'filters', + 'data' => [] + ], + [ + 'view' => 'content/product/product_details', + 'key' => 'product', + 'data' => $product + ] + ] + ] + ]); + + } + + +} diff --git a/html/app/controllers/Resolve.php b/html/app/controllers/Resolve.php new file mode 100644 index 0000000..7b9222c --- /dev/null +++ b/html/app/controllers/Resolve.php @@ -0,0 +1,91 @@ +<?php + +namespace app\controllers; + +use \Benchmark; + +/** Resolve class + * will resolve ambiguous utl-request patterns + * --- + * Then it can route the request to some controller; + * Alternatively it can proccess the request via Models + * and initiate the View engine + */ +class Resolve { + + public static function url($array) + { + $fullUrl = implode('/', $array); + $url = $array[count($array) - 1]; + + + if (!PRODUCTION) Benchmark::add_spot('proxy-page'); + + // proxying check if page with this url + $page = proxy( + [\app\models\cms\Page_model::class, 'fromUrl'], + [ $fullUrl ], CACHE_ROOT_TTL, + PROXY_CACHE_ERRORS + ); + // if page exist, construct page + if ($page !== false) { + // page exists, render page + load_template('base', [ 'page' => [ + 'title' => $page['Title'], + 'seo' => [], + 'sections' => [ + [ + 'view' => 'content/static', + 'key' => 'page', + 'data' => $page + ] + ] + ] + ]); + return ; + } + + + if (!PRODUCTION) Benchmark::add_spot('proxy-category'); + + // proxying check if products category with this url + $category = proxy( + [\app\models\market\ProductCategories_model::class, 'categoryFromUrl'], + [ $fullUrl ], CACHE_CATEGORY_TTL + ); + // if category exist, construct category + if ($category !== false) { + // render page + load_template('products', [ 'data' => [ + 'title' => $category['Title'], + 'seo' => [], + 'tree' => proxy( + [\app\models\market\ProductCategories_model::class, 'tree'], + [], CACHE_ROOT_TTL + ), + 'hierarchyPath' => explode('.', substr($category['Hierarchy'], 1, -1)), + 'sections' => [ + [ + 'view' => 'content/product_list/Product_List_Filters', + 'key' => 'filters', + 'data' => $category + ], + [ + 'view' => 'content/product_list/product_list', + 'key' => 'products', + 'data' => $category['products'] + ] + ] + ] + ]); + return ; + } + + + // not page nor category?: reply 404 + header("HTTP/1.0 404 Not Found"); + render_view('error/404', ['message' => 'Say something I\'m givin\' up on you.']); + + } + +}
\ No newline at end of file diff --git a/html/app/controllers/admin/.gitkeep b/html/app/controllers/admin/.gitkeep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/html/app/controllers/admin/.gitkeep diff --git a/html/app/controllers/api/.gitkeep b/html/app/controllers/api/.gitkeep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/html/app/controllers/api/.gitkeep diff --git a/html/app/controllers/api/Common_api.php b/html/app/controllers/api/Common_api.php new file mode 100644 index 0000000..a8bd75f --- /dev/null +++ b/html/app/controllers/api/Common_api.php @@ -0,0 +1,281 @@ +<?php + +namespace app\controllers\api; + +use \Registry; +use app\models\market\Product_model; +use app\models\market\ProductCategories_model; + +/** Common_api + * --- + * Controller for handling common API calls + * about main tables of the project; + * + * includes aglo for passing special filters and sorting + * (check parse_Where_OrderBy() method) + */ +class Common_api +{ + + /** APITables + * Method returns allowed tables to accept API calls + * The return is an array of [label => data-source] pairs + * where 'label' is a friendly name of the datasource + * (and 'datasource' the actual table/data-source) + */ + public static function APITables() + { + /** allowed tables for api call + * @return: (array) an of api-call arrays + * each api-call array has the folloing form: + * [ label => [ + * 'table' => (string) actual table in database, + * (optional) 'filter' => (array) of allowed filtering fields, + * (optional) 'sort' => (array) of allowed sorting fields, + * ] + * ] + */ + return [ + 'page' => [ + 'table' => 'pages', + 'filter' => ['Title'] + ], + 'product' => [ + 'table' => 'products', + 'filter' => [ 'ID', 'Title' ], + 'sort' => ['ID'] + ], + 'category' => [ + 'table' => 'product_categories', + 'filter' => ['Title', 'Hierarchy', 'Level'] + ] + ]; + } + + + /** select anything on any (allowed table) + * + supports filtering and sorting + * check self::parse_Where_OrderBy() for options; + * sets a limti of 1000 records + */ + public static function table($table) + { + $sources = self::APITables(); + if (array_key_exists($table, $sources)) { + + $query = Registry::get('REQUEST')->QUERY; + + // Get parametres? => parse filters + if ($query !== false) { + + $parsed = self::parse_Where_OrderBy( + $query, + ($sources[$table]['filter'] ?? []), + ($sources[$table][ 'sort' ] ?? []) + ); + $where = $parsed['filter'] ? (' WHERE ' . $parsed['filter']) : ''; + $orderBy = $parsed['sort'] ? (' ORDER BY '. $parsed['sort']) : '' ; + $bindArguments = $parsed['bind']; + + } else { + $where = ''; + $orderBy = ''; + $bindArguments = []; + } + + $db = Registry::use('database'); + $result = $db->runQuery('SELECT * + FROM '. $sources[$table]['table'] + . $where + . $orderBy + .' LIMIT 1000', + $bindArguments + ); + + reply_json([ + 'success' => true, + 'client' => Registry::get('REQUEST')->SIGNATURE, + 'result' => $result + ]); + + } else { + reply_json(['success' => false]); + } + die(); + } + + + /** get record of {table} by ID + * (if table has no ID then return false) + */ + public static function record($table, $id) + { + $sources = self::APITables(); + if (array_key_exists($table, $sources)) { + + $db = Registry::use('database'); + $result = $db->runQuery("SELECT * + FROM ". $sources[$table]['table'] ." + WHERE ID = :id", + [':id' => $id] + ); + reply_json([ + 'success' => true, + 'data' => $result[0] + ]); + + } else { + reply_json(['status' => false]); + + } + die(); + } + + + /** category_by_url + * product categorie by full-friendly-URL + * @param $url (string): full friendly url + */ + public static function category_by_url($url) + { + $category = proxy( + [\app\models\market\ProductCategories_model::class, 'categoryFromUrl'], + [ $url ], CACHE_CATEGORY_TTL + ); + if ($category !== false) { + reply_json([ + 'success' => true, + 'result' => $category + ]); + + } else { + reply_json(['status' => false]); + + } + die(); + } + + /** parse Where & OrderBy + * ------------------------------------------------------------------------- + * parses SAFELY the query string to Where {CONDITIONS} and ORDER BY clauses + * according to the specified rules. + * + * The rules: + * ** filters: ?fieldname=[operator]:value &... + * where operators:[ like | startlike | endlike | eq | gt | gteq | lt |t leq ] + * + * ** Order by: ?_sort=fieldname[:[asc|desc]][,field[,]] + * + * for example: ?id=gt:4&active=1&_sort:reputation:desc,category + * parses to WHERE id > 4 AND active = 4 ORDER BY reputation desc, catetory asc + * + * ------------------------------------------------------------------------- + * arguments: + * @param $query (string): string to be parsed + * @param $filters (array): the allowed fields to apply filters + * @param $sortings (array): the allowed fields to sort the result + * @return array('filter'=>(string) , 'sort'=>(string) , 'bind'=>(array)) + */ + private static function parse_Where_OrderBy($query, $filters=[], $sortings=[]) + { + // break query to [key => value] pairs + parse_str($query, $queryArray); + + $filterClause = []; // array to hold filter/WHERE clauses + $sortClause = []; // array to hold sort/ORDER-BY caluses + $bindings = []; // array to hold variable bindigs + + // parse filers + // --------------------------------------------------------------------- + foreach($queryArray as $filter => $value) { + + if (in_array($filter, $filters)) { + + $parts = explode(':', $value ); // that is => [operator], value + if (count($parts) == 0) { + // forget it + + } else if (count($parts) == 1) { + $filterClause[] = "{$filter} = :{$filter}"; + $bindings[$filter] = $parts[0]; + + } else { + + switch ($parts[0]) { + case 'like': + $filterClause[] = "{$filter} LIKE :{$filter}"; + $bindings[':'.$filter] = '%'.$parts[1].'%'; + break; + + case 'startlike': + $filterClause[] = "{$filter} LIKE :{$filter}"; + $bindings[':'.$filter] = $parts[1].'%'; + break; + + case 'endlike': + $filterClause[] = "{$filter} LIKE :{$filter}"; + $bindings[':'.$filter] = '%'.$parts[1]; + break; + + case 'gt': + $filterClause[] = "{$filter} > :{$filter}"; + $bindings[':'.$filter] = $parts[1]; + break; + + case 'gteq': + $filterClause[] = "{$filter} >= :{$filter}"; + $bindings[':'.$filter] = $parts[1]; + break; + + case 'lt': + $filterClause[] = "{$filter} < :{$filter}"; + $bindings[':'.$filter] = $parts[1]; + break; + + case 'lteq': + $filterClause[] = "{$filter} <= :{$filter}"; + $bindings[':'.$filter] = $parts[1]; + break; + + case 'eq': + default: + $filterClause[] = "{$filter} = :{$filter}"; + $bindings[':'.$filter] = $parts[1]; + } + } + } + } + $whereSQL = ($filterClause == []) + ? false + : implode(' AND ', $filterClause); + + + // parse sort options + // --------------------------------------------------------------------- + if (isset($queryArray['_sort'])) { + $sortTerms = explode(',', $queryArray['_sort']); + + foreach($sortTerms as $term) { + + $parts = explode(':', $term); + if ($parts != [] && in_array($parts[0], $sortings) ) { + $sortClause[] = (count($parts)==1) + ? $parts[0] + : $parts[0] .' '. (($parts[1] == 'desc') ? 'desc' : 'asc'); + } + } + + } + $sortSQL = ($sortClause == []) + ? false + : implode(', ', $sortClause); + + return ([ + 'filter' => $whereSQL, + 'sort' => $sortSQL, + 'bind' => $bindings + ]); + + } + +}
\ No newline at end of file diff --git a/html/app/controllers/api/Doc_api.php b/html/app/controllers/api/Doc_api.php new file mode 100644 index 0000000..e893552 --- /dev/null +++ b/html/app/controllers/api/Doc_api.php @@ -0,0 +1,34 @@ +<?php + +namespace app\controllers\api; + +use app\models\market\ProductCategories_model; +use app\models\Jorge; + +class Doc_api +{ + + /** */ + public static function tree($store = 904) + { + $tree = proxy( + [ProductCategories_model::class,'tree'], + [], CACHE_ROOT_TTL, + PROXY_IGNORE_CACHE + ); + reply_json([ 'success' => true, 'result' => $tree ]); + die(); + } + + + public static function dbDoc() + { + $j = new Jorge(); + reply_json([ + 'success' => true, + 'result' => $j->databaseDocumentation() + ]); + die(); + + } +}
\ No newline at end of file diff --git a/html/app/controllers/cli/CliContoller.php b/html/app/controllers/cli/CliContoller.php new file mode 100644 index 0000000..310e2ae --- /dev/null +++ b/html/app/controllers/cli/CliContoller.php @@ -0,0 +1,85 @@ +<?php + +namespace app\controllers\cli; + +/** CliContoller + * + * Re-generate cashes of commonly used entities + * like: product categories and trendy products + * (proxy calls make use of PROXY_IGNORE_CACHE) + * + * CliController calls Model methods who create + * cashed data with the exact same arguments as + * when called from the web interface; this way + * the Model::method(arguments) triplete creates + * the same keys as via the web interface. + * + * NOTE: Only Cli interface is allowed + * [if (php_sapi_name() != 'cli') return false;] + * + * The Cli-interfaced script can run manualy or + * (most usual scenario) scheduled via a cron job + * in order to refresh cashes befor expiration. + * + * --------------------------------------------- + */ +class CliContoller +{ + + + /** (re-) cacheCategoriesTree + * regenerate cache for product_categories tree + * @param (void) + */ + public static function cacheCategoriesTree() + { + // Only Cli interface is allowed + if (php_sapi_name() != 'cli') return false; + + # PROXY call: + # MARKET \ ProductCategories_model::tree(): + # + Refresh Cache + # -------------------------------------- + echo textColor("Creating category_products tree Cache ... ", \NORMAL); + $reply = proxy( + [\app\models\market\ProductCategories_model::class, 'tree'], + [], \CACHE_ROOT_TTL, + \PROXY_IGNORE_CACHE + ); + echo ($reply == false) ? textColor("Failed\n", \FAIL) : textColor("Done!\n", \SUCCESS); + } + + + + public static function getProductCategories() + { + return \Registry::use('database')->runQuery( + "SELECT * FROM product_categories", + [] + ); + } + + + + /** (re-) cacheCategoryByUrl( url ) + * regenerate cache of a category_product + * by category's friendly url + * @param $url (string): the category's friendly url + */ + public static function cacheCategoryByUrl(string $url) + { + // Only Cli interface is allowed + if (php_sapi_name() != 'cli') return false; + + # PROXY call: + # MARKET \ ProductCategories_model::tree(): + # + Refresh Cache + # -------------------------------------- + $reply = proxy( + [\app\models\market\ProductCategories_model::class, 'categoryFromUrl'], + [ $url ], \CACHE_CATEGORY_TTL, + \PROXY_IGNORE_CACHE + ); + echo ($reply == false) ? textColor("Failed\n", \FAIL) : textColor("Done!\n", \SUCCESS); + } +}
\ No newline at end of file diff --git a/html/app/models/Jorge.php b/html/app/models/Jorge.php new file mode 100644 index 0000000..011b8c0 --- /dev/null +++ b/html/app/models/Jorge.php @@ -0,0 +1,169 @@ +<?php + +namespace app\models; + +use \Registry; + +/** Jorge + * a database reference and adminitration toolkit + * --- + * The name Jorge comes from Uberto Eco's book: 'il nome della rosa', + * (Jorge is the blind monk who supervises the monastery's library ;) + */ +class Jorge +{ + + /** tables + * array to keep all database-tables' critical information. + * @key create (array) includes all 'CREATE TABLE' statements + * @key relate (array) includes relations between tables + * @key fields (array) includes all important fields (when no select*) + * @key filter (array) includes all fields used to filter results + */ + private $tables = [ + 'create' => [], + 'relate' => [], + 'fields' => [] + ]; + + public function __construct() + { + $db = Registry::use('database'); + + // get all tables + $tableList = $db->runQuery("SHOW TABLES", []); + + // get all CREATE TABLE statements + foreach($tableList as $tableArray) { + $table = array_shift($tableArray); + $create = $db->runQuery("SHOW CREATE TABLE {$table}", []); + $this->tables['create'][$table] = $create[0]['Create Table']; + } + + // define relations + $this->tables['relate'] = [ + + 'products' => [ + 'brands' => 'products.BrandID = brands.ID', + 'products_to_product_categories' => 'products.ID = products_to_product_categories.SimpleProductID', + 'products_to_images' => 'products.ID = products_to_images.SimpleProductID', + 'prices' => 'products.SKU = prices.SKU' + ], + + 'products_to_product_categories' => [ + 'product_categories' => 'product_categories.ID = products_to_product_categories.ProductCategoryID' + ], + + 'products_to_images' => [ + 'products' => 'products_to_images.SimpleProductID = products.ID', + 'assets' => 'products_to_images.ImageID = assets.ID' + ] + + ]; + + $this->extractTableFields(); + + // return true; + } + + + /** Create... + * one or more database tables + * --- + * @param $datasource (string|void) : table name or none + * if none then all tables will be created + * @return (boolean) + * + * NOTE: + * for safety reasons this method is not executing SQL; + * it just echoes the SQL that needs to be executed in + * order to crate all database tables. + */ + public function create($datasource = '') + { + // if no datasource passed then datasource is all tables + if ($datasource == '') { + $datasource = array_keys(self::$tables['create']); + + } else { + // if datasource exist, make it an array + if (array_key_exists($datasource, self::$tables['create'])) { + $datasource = [ $datasource ]; + + } else { + // table does not exist + return fasle; + } + } + + $sql = ""; + foreach( $datasource as $table ) { + $sql .= "-- CREATE ". $table .";\n". self::$tables['create'][$table] . "\n\n"; + } + + return ['sql' => $sql]; + } + + + /** Calculate fields of every table + * --- + * Parse every CREATE SQL statement and extract list of fields; + * Algorithm implements linear-parsing (faster than a recursive one) + */ + private function extractTableFields() + { + // words used by SQL that can not be field names + $nonFields = ['PRIMARY', 'KEY', '(', ')']; // reduced list (used on CREATE) + + foreach( $this->tables['create'] as $table => $sqlCreate ) { + + $fields = []; // variable to hold the extracted fields + + // get text between first open-parentesis and last close-parentesis + // this is waht lies between 'CREATE TABLE table(' and ') [ENGINE whatever]' + // (including the patenteses) + preg_match_all( + "/\((((?>[^()]+)|(?R))*)\)/", + str_replace("\n", '', $sqlCreate), + $match + ); + // remove 1st+last parentesis + $mainDefinitions = substr($match[0][0], 1, -1); + + // replace comma (,) on decimal definitions + // example: 'decimal(18, 2)' turns to 'decimal(18: 2)' + $sentences = preg_replace( + '/\\(([0-9]*)[ ,]([0-9 ]*)\\)/', + '($1:$2)', + $mainDefinitions, + -1 + ); + + // devide the banth of sentences to field definitions + $defines = explode(',', $sentences); + + // now each denine holds a full field definition + // like: `ID` int(11) NOT NULL AUTO_INCREMENT + + foreach($defines as $def) { + // check the first term of each sentence + $terms = explode(' ', trim($def,)); + $term = array_shift($terms); + + if (!in_array($term, $nonFields)) { + $fields[ str_replace('`', '', $term) ] = implode(' ', $terms); + } + } + + $this->tables['fields'][$table] = $fields; + } + + return; + } + + public function databaseDocumentation() + { + return $this->tables['fields']; + } + +}
\ No newline at end of file diff --git a/html/app/models/_info.md b/html/app/models/_info.md new file mode 100644 index 0000000..b5b2e1c --- /dev/null +++ b/html/app/models/_info.md @@ -0,0 +1,84 @@ + +# info about the structrure of the models + +According to the former impementation (atcom) the project will need to handle 200+ tables. +The former project includes 209 tables with 5 or more rows (and 325 tables totaly) + + + +## Partitioning the database schema + +Models shall organized in various *thematic entity-groups* in order to handle +the numerous relationd between tables. + +* market ~10 + - [Ok] - products = cms_eStore_Products + - [Ok] - cms_Relationship_SimpleProducts_ProductCategories + - [Ok] - product_categries = cms_eStore_ProductCategories + - [Ok] - cms_eStore_PriceLists + - [Ok] - cms_eStore_Prices, + - [Ok] - products_to_images (assets) + - [Ok] - assets (*cms) + - [Ok] -brands = cms_eStore_Brands + +* sales ~ 15 + - orders = cms_eStore_Orders + - orderItems = cms_eStore_OrderItems + - payments + - refunds + - tracking + - orderEvents = cms_eStore_OrderEventLogs + - cms_eStore_OrderStatusDescriptions, + +* care + - [data] - customers = cms_YodaAddon_Customers + - [data] - addresses = cms_YodaAddon_Addresses + - [data] - userlists = cms_YodaAddon_UserLists + +* system ~ 5 + - security + - operators + - sessions * cms_UserSessions + - tokens + +* cms ~20 + - [?Ok] - about = cms_Pages + - stores = cms_YodaAddon_Stores + - cms_Relationship_Stores_Services + - cms_Relationship_Stores_Images + - cms_Relationship_Stores_Services + - news + - contests + - [Ok] - assets + - snippets, translations, whatever... + + +Then there shall be connecting-models betweem various entities, like: + +* Marketing: combines [market + cms] + +* Buing: combines [market + sales + care] + + + +## uncategorized: + +* cms_YodaAddon_Companies + + + + +NOTE: + +Also need to partition the "working-on/preccessing data" + +- CMS : content creation, design, etc + +- Orders : pick, select, track, manare returns, etc + +- Customers: navigate on store, fill basket, order, pay + +- Prices: ERP + +I suggest each group of proccessing is make by different application. +These applications may *speak* to eachother via API diff --git a/html/app/models/cms/Page_model.php b/html/app/models/cms/Page_model.php new file mode 100644 index 0000000..fda5fb1 --- /dev/null +++ b/html/app/models/cms/Page_model.php @@ -0,0 +1,18 @@ +<?php + +namespace app\models\cms; + +use \Registry; + +class Page_model +{ + public static function fromUrl($url) + { + return Registry::use('database')->query( + "SELECT * FROM pages + WHERE FullFriendlyUrl = :url AND IsActive = 1", + [ ':url' => $url ] + )->getFirst(); + } + +}
\ No newline at end of file diff --git a/html/app/models/market/Market_repository.php b/html/app/models/market/Market_repository.php new file mode 100644 index 0000000..1338426 --- /dev/null +++ b/html/app/models/market/Market_repository.php @@ -0,0 +1,235 @@ +<?php + +namespace app\models\market; + +use \Repository; + +/** Market_repository + * (thematic-entity repository) + * --- + * SQL entity templates for Market + * + * Every '_base' is a repository of common 'prepare' SQL-queries; + * They usually include holders for attaching/binding data safely; + * These queries are expensive in syntax but fast on run time and + * are used to collect multi-joined amounts ot data. + * + * Organize yous repositories in logical/thematic groups (partitioning). + * This way your repository is managable and uses very few sources. + * + * This class acts like a namespaced repository for geting your code readable + * Avoid pushing very simple queries into repository classes. A record like... + * 'CARS_by:Age' => "SELECT * FROM cars WHERE age = :Age" + * ... won't make your code sexier nor easier to read; it will just increase + * your label's entropy and make trickier to pick your next sensable label; + * + * repository classes include two methods (inherited by the abstract, so + * you do not need to implement something more than just construct + * the $repository array) + * ** pull: for pulling a query by a friendly name + * ** echo: optional method used by some devolopment tools + * + * TODO: development tool using echo is still in progress + */ +class Market_repository extends Repository +{ + //// use Repository; // repository trait includes + //// // a method to pull an entity of the gathering + //// // and method to (TODO:) ... + + /** repository is an array of Prepare SWL queries; + * each query is attached to the array via a friendly label; + * + * NOTE: (PROPOSAL) + * --- + * about the label: + * 1. keep names short descriptive but not too long + * 2. main entity/entities shall be UPPERCASED + * 3. determinands/adjectives shall be Cappitalized + * 4. words are connected with underscore (_) + * 5. if SQL inludes bind-holderd, label shall be suffixed with '_by:' + * followed by holder names connected with undercores (case-sensitively) + * + * about the data-bind holders (if present): + * 1. all holders are prefixed with ':' + * 2. holder name shall begin with a letter and contain one word. + * + * example: + * 'Active_EMPLOYEES_by:Age_CityID' => "SELECT Em.*, c.* + * FROM employee Em LEFT JOIN city c ON c.id = Em.city_id + * WHERE c.id = :CityID AND Em.age = :Age" + * + * It is recommended to leave a descriptive comment befor each declaration + * and you may also include sql-comments inside the query. + */ + public static $repository = [ + + // PRODUCTS by: storeID, ProductUrl + // picks all product properties + // + default image + price for certain store + 'Active_PRODUCT_by:storeID_ProductUrl' => + "SELECT p.*, + pc.ID AS ProductCategory, + pc.Hierarchy, + pc.FullFriendlyUrl AS `Path`, + prices.ProductPrice_OriginalPrice AS Price, + prices.ProductPrice_DiscountedPrice AS DiscountPrice, + prices.UnitMeasurePrice_OriginalPrice AS PerUnitPrice, + prices.UnitMeasurePrice_DiscountedPrice AS PerUnitDiscountPrice, + b.Title BrandName + FROM products p + LEFT JOIN products_to_product_categories p2pc ON p.ID = p2pc.SimpleProductID + LEFT JOIN product_categories pc ON p2pc.ProductCategoryID = pc.ID + LEFT JOIN prices ON p.SKU = prices.SKU + LEFT JOIN products_to_images p2i ON p2i.SimpleProductID = p.ID + LEFT JOIN assets ON assets.ID = p2i.ImageID + LEFT JOIN brands b ON b.ID = p.BrandID + WHERE pc.IsActive = 1 AND pc.IsCurrentlyActive = 1 + AND p2i.Order = 1 + AND p.IsActive = 1 + AND p.Published = 1 + AND prices.PriceListID = :storeID + AND p.FriendlyUrl = :ProductUrl" + , + + // PRODUCTS by: storeID, ProductID + // picks all product properties + // + default image + price for certain store + 'Active_PRODUCT_by:storeID_ProductID' => + "SELECT p.*, + pc.ID AS ProductCategory, + pc.Hierarchy, + pc.FullFriendlyUrl AS `Path`, + prices.ProductPrice_OriginalPrice AS Price, + prices.ProductPrice_DiscountedPrice AS DiscountPrice, + prices.UnitMeasurePrice_OriginalPrice AS PerUnitPrice, + prices.UnitMeasurePrice_DiscountedPrice AS PerUnitDiscountPrice, + b.Title BrandName + FROM products p + LEFT JOIN products_to_product_categories p2pc ON p.ID = p2pc.SimpleProductID + LEFT JOIN product_categories pc ON p2pc.ProductCategoryID = pc.ID + LEFT JOIN prices ON p.SKU = prices.SKU + LEFT JOIN products_to_images p2i ON p2i.SimpleProductID = p.ID + LEFT JOIN assets ON assets.ID = p2i.ImageID + LEFT JOIN brands b ON b.ID = p.BrandID + WHERE pc.IsActive = 1 AND pc.IsCurrentlyActive = 1 + AND p2i.Order = 1 + AND p.IsActive = 1 + AND p.Published = 1 + AND prices.PriceListID = :storeID + AND p.ID = :ProductID" + , + + // CATEGORY-PRODUCTS by: storeID, likeHierarchy + // traverses all products from category and subcategories + // picke price and default image + 'CATEGORY-PRODUCTS_by:storeID_likeHierarchy' => + "SELECT p.*, + assets.Url AS ImageUrl, + pc.FullFriendlyUrl AS `Path`, + prices.ProductPrice_OriginalPrice AS Price, + prices.ProductPrice_DiscountedPrice AS DiscountPrice, + prices.UnitMeasurePrice_OriginalPrice AS PerUnitPrice, + prices.UnitMeasurePrice_DiscountedPrice AS PerUnitDiscountPrice + FROM products p + LEFT JOIN prices ON p.SKU = prices.SKU + LEFT JOIN products_to_product_categories p2pc ON p2pc.SimpleProductID = p.ID + LEFT JOIN product_categories pc ON pc.ID = p2pc.ProductCategoryID + LEFT JOIN products_to_images p2i ON p2i.SimpleProductID = p.ID + LEFT JOIN assets ON assets.ID = p2i.ImageID + WHERE p2pc.ProductCategoryID IN ( + SELECT `ID` FROM product_categories + WHERE Hierarchy LIKE :likeHierarchy + ) + AND p2i.Order = 1 + AND p.IsActive = 1 + AND p.Published = 1 + AND prices.PriceListID = :storeID" + , + + // Count Products + // of all last-lever Product categories + // by StoreID + // --- + // used on creating product_categories tree + // ProductCategories_model::tree() + 'Count_Last_Level_PRODUCTS_by:storeID' => + "SELECT pc.ID, COUNT(p.ID) as `Counter` + FROM product_categories pc + LEFT JOIN products_to_product_categories p2pc ON pc.ID = p2pc.ProductCategoryID + LEFT JOIN products p ON p2pc.SimpleProductID = p.ID + LEFT JOIN prices ON p.SKU = prices.SKU + LEFT JOIN products_to_images p2i ON p2i.SimpleProductID = p.ID + LEFT JOIN assets ON assets.ID = p2i.ImageID + WHERE pc.IsActive = 1 AND pc.IsCurrentlyActive = 1 + AND p2i.Order = 1 + AND pc.Level = 2 + AND p.IsActive = 1 + AND p.Published = 1 + AND prices.PriceListID = :storeID + GROUP BY pc.ID" + , + + // PRODUCT by: ProductID + // Join images array (json string) + // Join prices for all stores (json string) + 'Active_PRODUCT_complete_by:ProductID' => + "SELECT p.*, pc.ID AS ProductCategory, + pc.Hierarchy, pc.FullFriendlyUrl AS `Path`, + ( -- get all store prices as Json + SELECT JSON_ARRAYAGG(JSON_OBJECT( + 'Store', prices.PriceListID, + 'Price', prices.ProductPrice_OriginalPrice, + 'DiscountPrice', prices.ProductPrice_DiscountedPrice, + 'PerUnitPrice', prices.UnitMeasurePrice_OriginalPrice, + 'PerUnitDiscountPrice', prices.UnitMeasurePrice_DiscountedPrice + )) + FROM prices + WHERE prices.SKU = p.SKU + ) as pricesJson, + ( -- get all images as Json + SELECT JSON_ARRAYAGG(JSON_OBJECT( + 'Order', p2i.Order, + 'Url', assets.Url, + 'Title', assets.Title, + 'Description', assets.Description + )) + FROM products_to_images p2i + LEFT JOIN assets ON assets.ID = p2i.ImageID + WHERE p2i.SimpleProductID = p.ID + ) AS imagesJson, + b.Title BrandName + FROM products p + LEFT JOIN products_to_product_categories p2pc ON p.ID = p2pc.SimpleProductID + LEFT JOIN product_categories pc ON p2pc.ProductCategoryID = pc.ID + LEFT JOIN products_to_images p2i ON p2i.SimpleProductID = p.ID + LEFT JOIN assets ON assets.ID = p2i.ImageID + LEFT JOIN brands b ON b.ID = p.BrandID + WHERE pc.IsActive = 1 AND pc.IsCurrentlyActive = 1 + AND p2i.Order = 1 + AND p.IsActive = 1 + AND p.Published = 1 + AND p.ID = :ProductID" + , + + // Product images by: ProductID + // (sorted by 'order' field) + 'PRODUCT_IMAGES_by:productID' => + "SELECT a.Url, a.Title, a.Description + FROM products p + LEFT JOIN products_to_images p2i ON p2i.SimpleProductID = p.ID + LEFT JOIN assets a ON a.ID = p2i.ImageID + WHERE p.IsActive = 1 + AND p.Published = 1 + AND p.ID = :productID + ORDER BY p2i.Order" + + ]; + + /* no need to impement anything else; + * you can overide if needed the default methods: + * + public static function pull($entity){ } + * + public static function echo($content =false){ } + */ + +} diff --git a/html/app/models/market/ProductCategories_model.php b/html/app/models/market/ProductCategories_model.php new file mode 100644 index 0000000..d8c274e --- /dev/null +++ b/html/app/models/market/ProductCategories_model.php @@ -0,0 +1,199 @@ +<?php + +namespace app\models\market; + +use \Registry; +use app\models\market\Market_repository; + + +/** ProductCategories_model + * --- + * undertakes to collect any product_categories data + * from the database and prepare needed data structures. + * + */ +class ProductCategories_model +{ + + /** Get (product_) category from (full-friendly_) URL + * --- (self explanatory) + * @param $url (string): Full-Friendly-URL + * @return $category (array); also includes all category products + */ + public static function categoryFromUrl($url) + { + $db = Registry::use('database'); + $category = $db->query( "SELECT * FROM product_categories + WHERE FullFriendlyUrl = :url AND IsActive = 1", + [ ':url' => $url ] + )->getFirst(); + + if ($category === false) { return false; } // no category? -> false + + // GET PRODUCTS of category + $category['products'] = self::categoryProductsByHierarchy($category['Hierarchy']); + return $category; + } + + + /** Get (product_) category from ID + * --- (self explanatory) + * @param $id (int): Catgory ID (usualy from products) + * @return $category (array); also includes all category products + */ + public static function categoryFromID($id) + { + return Registry::use('database')->query( "SELECT * FROM product_categories + WHERE `ID` = :url AND IsActive = 1", + [':id' => $id] + )->getFirst(); + + } + + + /** categoryProductsByHierarchy + * --- + * This method returns the products belonging to a certain + * category (and all it's the children/sub-categories); + * It uses category.Hierarchy in a WHERE LIKE condition to + * make it fast. + * + * The method is category.Level agnostic + * + * @param $category (array) + * @return $products (array) + */ + public static function categoryProductsByHierarchy($hierarchy, $store = 904) + { + // GET PRODUCTS of category + // NOTE: + // category.Hierarchy is used + + return Registry::use('database')->runQuery( + Market_repository::pull('CATEGORY-PRODUCTS_by:storeID_likeHierarchy'), + [ + ':likeHierarchy' => $hierarchy.'%', + ':storeID' => 904 + ] + ); + } + + + # depricated: + # now Proxy has the responsibility to cache whatever + # --- + # /** GET tree of product_categories + # * --- + # * get from cache or cache it after creation + # */ + # public static function tree($store = 904) + # { + # $cacheKey = get_called_class() . $store .'/tree'; + # + # // IF category CACHED + # $cache = Registry::use('cache'); + # $tree = $cache->get($cacheKey); + # if ($tree !== false) { + # return $tree; + # } + # + # // (not cached) Create and Cache it + # $tree = self::createCategoriesTree(); + # $cache->set($cacheKey, $tree, CACHE_ROOT_TTL); + # + # return $tree; + # } + + + /** CREATE product_categories tree + * NOTE: includes active categories only; + * + * Algorith uses 'Hierarchy' for category path-positioning, + * implementing a linear conctruction of the requested tree; + * (faster and less source-consuming than a recursive algo) + */ + public static function tree($store = 904) + { + $tree = []; // variable to hold results + + $db = Registry::use('database'); + + // get raw categories data --------------------------------------------- + + $raw = $db->runQuery( "SELECT `ID`, Title, `Level`, Hierarchy, + ParentID, `Order`, FullFriendlyUrl + FROM product_categories pc + WHERE IsActive = 1 AND IsCurrentlyActive = 1 + ORDER BY pc.Level asc, pc.Order asc, pc.Hierarchy asc", + [] + ); + + // count products per 3rd level category ------------------------------- + + $countProducts = $db->runQuery( + Market_repository::pull('Count_Last_Level_PRODUCTS_by:storeID'), + [ ':storeID' => $store ] + ); + // map categoryID -> Counter + $mapCounter = []; + foreach($countProducts as $rec) $mapCounter[$rec['ID']] = $rec['Counter']; + + // parse raw data to hierarchical tree (2 pass) ------------------------ + + // 1st pass: parse raw data, construct and fill the $tree array ........ + foreach($raw as $rec) { + + // create category path from Hierarchy (format: .10.10100. ) + // so remove 1st and last dots (.) from Hierarchy string + // then explode via dot-character (.) + $categoryPath = explode('.', substr($rec['Hierarchy'], 1, -1)); + + switch ($rec['Level']) { + case 0: + $tree[$categoryPath[0]] = [ + 'info' => $rec, + 'childs' => [] + ]; break; + + case 1: + $tree[$categoryPath[0]]['childs'][$categoryPath[1]] = [ + 'info' => $rec, + 'childs' => [] + ]; break; + + case 2: + $tree[$categoryPath[0]]['childs'][$categoryPath[1]]['childs'][$categoryPath[2]] = [ + 'info' => $rec, + 'count' => isset($mapCounter[$categoryPath[2]]) ? $mapCounter[$categoryPath[2]] : 0 + ]; break; + + default: + // nothing... + } + + } + + // 2nd pass: remove categories with no products ........................ + foreach($tree as $id0 => $rec0) { + $count0 = 0; + + foreach($rec0['childs'] as $id1 => $rec1) { + $count1 = 0; + + foreach($rec1['childs'] as $id2 => $rec2) { + if ($rec2['count']>0) $count1++; + else unset($tree[$id0]['childs'][$id1]['childs'][$id2]); + } + + if ($count1 > 0) $count0++; + else unset($tree[$id0]['childs'][$id1]); + } + + if (!$count0) unset($tree[$id0]); + } + + return $tree; + } + + +}
\ No newline at end of file diff --git a/html/app/models/market/Product_model.php b/html/app/models/market/Product_model.php new file mode 100644 index 0000000..aa554a1 --- /dev/null +++ b/html/app/models/market/Product_model.php @@ -0,0 +1,49 @@ +<?php + +namespace app\models\market; + +use \Registry; +use app\models\market\Market_repository as Market; + +class Product_model +{ + public static function fromUrl($url, $store = 904) + { + $product = Registry::use('database')->query( + Market::pull('Active_PRODUCT_by:storeID_ProductUrl'), + [ + ':storeID' => $store, + ':ProductUrl' => $url + ] + )->getFirst(); + + // if no product, return false + if ($product === false) return false; + + // inject product's images + $product['Images'] = self::getProductImages($product['ID']); + + return $product; + } + + + public static function getProductImages($id) + { + ## return Registry::use('database')->runQuery( + ## "SELECT a.Url, a.Title, a.Description + ## FROM products p + ## LEFT JOIN products_to_images p2i ON p2i.SimpleProductID = p.ID + ## LEFT JOIN assets a ON a.ID = p2i.ImageID + ## WHERE p.IsActive = 1 AND p.Published = 1 + ## AND p.ID = :id + ## ORDER BY p2i.Order", + ## [ ':id' => $id ] + ## ); + + return Registry::use('database')->runQuery( + Market::pull('PRODUCT_IMAGES_by:productID'), + ['productID' => $id ] + ); + } + +}
\ No newline at end of file diff --git a/html/app/models/todo.md b/html/app/models/todo.md new file mode 100644 index 0000000..defd21e --- /dev/null +++ b/html/app/models/todo.md @@ -0,0 +1,132 @@ + + +Privilege +--- + +Privilege::list_of_privileges( privilege_id ) + +Privilege::privileges_tree() + + + + + +User +--- + +User::get_privileges + + +User::has_privilege( privilege_id ) + + +User::reset_password() + -> create otp + -> send email + + +User::set_privilege( privilege_id ) + + +User::track_action() + -> user_id + -> action : Contoller::method(args) + -> timestamp + + + +Lesson: +--- + +Lesson::create_lesson( POST ) + +Lesson::get_lesson( lesson_id ) + + + +Course +--- + +Course::create_course( POST ) + +Course::courses_tree() + + + +Upgrades (??) +--- + + +A1 + +A2 : includes A1 + +A3 : includes A1, A2 + +A4 : includes A1, A2, A3 + + +A1 100 +A2 200 +A3 350 +A4 500 + +B1 100 +B2 200 +B3 400 +B4 800 + + +:1 -> 0% +:2 -> 15% +:3 -> 20% + + + + +A1 -> A2 +A1 -> A3 +A1 -> A4 + +A2 -> A3 +A2 -> A4 + +A3 -> A4 + +A1 < A2 < A3 < A4 + + + +B1 -> B2 +B1 -> B3 +B1 -> B4 + +B2 -> B3 +B2 -> B4 + +B3 -> B4 + + +A1+B1 + +A2+B2 + +A3+B3 + + +A1+B1 -> A2+B2 +A1+B1 -> A3+B3 +A1+B1 -> A4+B4 + +A2+B2 -> A3+B3 +A2+B2 -> A4+B4 + +A3+B3 -> A4+B4 + + + +UPGRADE PRICE = upper_priv_price - own_priv_price + +buy 1 item : -0% +buy 2 items : -10% +buy 3 items : -15% diff --git a/html/app/routes/api.php b/html/app/routes/api.php new file mode 100644 index 0000000..659c25f --- /dev/null +++ b/html/app/routes/api.php @@ -0,0 +1,64 @@ +<?php + +use app\controllers\api\Common_api; +use app\controllers\api\Doc_api; + +// api: tree of product-categories +Route::add('/api/tree', + function() { Doc_api::tree(); }, + 'get' +); + +// api: database documentation +Route::add('/api/doc', + function() { Doc_api::dbDoc(); }, + 'get' +); + +Route::add('/api/url/([0-9a-zA-Z-_\/]*)', + function($url) { Common_api::category_by_url($url); } +); + +// route: /api/table +Route::add('/api/([0-9a-zA-Z-_]*)', + function($table) { Common_api::table($table); }, + 'get' +); + +// route: /api/table/{id} +Route::add('/api/([0-9a-zA-Z-_]*)/([0-9]*)', + function($table, $id) { Common_api::record($table, $id); }, + 'get' +); + + +# // test on the go +# // find products with large differences on price between stores +# // --- +# Route::add('/api/price-diffs', function() { +# $wtf = Registry::use('database')->runQuery("SELECT sku, +# max(ProductPrice_OriginalPrice) as MX, +# min(ProductPrice_OriginalPrice) as MN, +# ((max(ProductPrice_OriginalPrice)-min(ProductPrice_OriginalPrice))/max(ProductPrice_OriginalPrice)) as Di +# FROM prices +# GROUP BY sku +# ORDER BY Di desc +# LIMIT 300", +# [] +# ); +# reply_json([ +# 'success' => true, +# 'result' => $wtf +# ]); die(); +# }); + + +// route for testing... +// --- +Route::add('/test/([0-9a-zA-Z-_\/]*)', function($test) { + if (file_exists(TESTS_DIRECTORY . $test .'.php')) { + if (!Benchmark::exist('start')) Benchmark::add_spot('start'); + require(TESTS_DIRECTORY . $test .'.php'); + } else { echo "Test {$test} not exist"; + } die(); +}); diff --git a/html/app/routes/backend.php b/html/app/routes/backend.php new file mode 100644 index 0000000..9b90cf1 --- /dev/null +++ b/html/app/routes/backend.php @@ -0,0 +1,9 @@ +<?php + +Route::add('/admin/info', // generaly you need to hide phpinfo() + function() { // so, remove this route on production + // Authenticate::session(); // or require authentication + phpinfo(); }, + 'get' +); + diff --git a/html/app/routes/frontend.php b/html/app/routes/frontend.php new file mode 100644 index 0000000..1eea46c --- /dev/null +++ b/html/app/routes/frontend.php @@ -0,0 +1,135 @@ +<?php + +use app\controllers\Resolve; +use app\controllers\Product; + + +// Home/Welcome +// --- +Route::add('/', function() { // echo 'Welcome!'; }); + header('Location: /eidi-artozacharoplasteioy/psomi-artoskeyasmata', true, 302); +}); + + +// Error Pages +// --- +Route::notFound( function() { + header("HTTP/1.0 404 Not Found"); + render_view('error/404', ['message' => 'nobody knows it (but you got a secret smile;)']); +}); // 404 + + +// Resolve friendly urls +// --- +Route::add('/([0-9a-zA-Z-_]*)', + function($a) { Resolve::url([$a]); } +); +Route::add('/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)', + function($a, $b) { Resolve::url([$a, $b]); } +); +Route::add('/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)', + function($a, $b, $c) { Resolve::url([$a, $b, $c]); } +); + + +// 4-th level paths belong to products +// --- +Route::add('/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)', + function($a, $b, $c, $d) { Product::fromUrl($d, [$a, $b, $c]); } +); + + +/** URL-format Proposal: + * ----------------------------------------------------------------------------- + * + * Page (almost-static content): + * /about/{url} +# Route::add('/about/([0-9a-zA-Z-_\/]*)', function($url) { Page::fromUrl($url); }); + * + * Product: + * /{level-1}/{level-2}/{level-3}/{friendly-url}-{id} +# Route::add('/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)-([0-9]*)', +# function($category, $subcategory, $group, $label, $id) { +# Product::fromID([$$category, $subcategory, $group, $label], $id); +# } +# ); + * + * Category + * /{category}/[ {subcategory}[ /{group}]] +Route::add('/([0-9a-zA-Z-_]*)', function($a) { Category::url([$a]); }; +Route::add('/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)', function($a, $b) { Category::url([$a, $b]); }); +Route::add('/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)', function($a, $b, $c) { Category::url([$a, $b, $c]); }); + * ----------------------------------------------------------------------------- + */ + + +/* EXAMPLES (use it for brainsrtorming) + * ----------------------------------------------------------------------------- + * + // Typical Use + // --- + Route::add('/art', function() { Art::blah(); }); + Route::add('/art/db', function() { Art::fromDatabase(); }); + Route::add('/art/([0-9]*)', function($id) { Art::getInfo($id); }); + + + // Get-Post route example + // --- + Route::add('/contact-form', function() { + echo '<form method="post"><input type="text" name="test" /><input type="submit" value="send" /></form>'; + }, 'get'); + + Route::add('/contact-form', function() { + echo 'Hey! The form has been sent:<br/>'; print_r($_POST); + }, 'post'); + + + // Accept number as parameter + // --- + Route::add('/foo/([0-9]*)/bar', function($var1) { + // echo $var1.' is a number!'; + echo "{$var1} is a number!"; + }); + + + // About pages + // --- + Route::add('/about/([0-9a-zA-Z-_]*)', function($page) { + InfoPage::render('about/'.page); + }); + + + // handle a request like: /foo/123/bar/3254-lefki-zaxari-marata-1kg + // where first-numeric-part of last-uri-section (3254) is the product number + // --- + Route::add('/foo/([0-9]*)/bar/([0-9]*)-([0-9a-zA-Z-_]*)', function($num, $id, $name) { + echo $num .' is some nomber, id = '. $id .' and label = '. $name; + }); + + + // handle a request like: /zaxares-glykantika/lefki-zaxari-year-2022-45678 + // where last-numeric-part of last-uri-section (45678) is the product number + // --- + Route::add('/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)-([0-9]*)', + function($category, $subcategory, $group, $label, $id) { + Shop::product([$$category, $subcategory, $group, $label], $id); + }); + + + // last chance to resolve (some friendly url) + // --- + Route::add('/([0-9a-zA-Z-_]*)', function($a) { Resolve::uri([$a]); }); + Route::add('/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)', function($a, $b) { Resolve::uri([$a, $b]); }); + Route::add('/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)', function($a, $b, $c) { Resolve::uri([$a, $b, $c]); }); + + + // Product page + // handle a request like: /pantopoleio/zaxares/lefki-zaxari/lefki-zaxari-year-2022-45678 + // where last-numeric-part of last-uri-section (45678) is the product number + // --- + Route::add('/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)-([0-9]*)', + function($category, $subcategory, $group, $label, $id) { + Shop::product([$$category, $subcategory, $group, $label], $id); + }); +* ------------------------------------------------------------------------------ +*/ diff --git a/html/app/views/basic.php b/html/app/views/basic.php new file mode 100644 index 0000000..3d9abde --- /dev/null +++ b/html/app/views/basic.php @@ -0,0 +1,53 @@ +<?php ob_start(); ?><!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>ΣΚΛΑΒΕΝΙΤΗΣ - <?=$page['title']?></title> + + <!-- import assets --> + <?php link_asset( + 'font', + ['iconfont', 'CFAstyStdBold', 'CFAstyStdBook', 'CFAstyStdMedium'], + 'crossorogin' + ) ?> + + <?php link_asset('css', ['main']); ?> + + <!-- TODO: parse seo data --> + + +</head> +<body class="homepage" data-plugin-lazyload> + + <div class="mainContainer"> + + <div class="mainLayer"></div> + + + <!-- header --> + < ?php cache_view('partials/header', []); ? > + <?php ob_flush(); ?> + + + <!-- main content --> + <main class="main"> + + <?php parse_sections( $page['sections'] ); ?> + + </main> + <?php ob_flush(); ?> + + + <!-- footer --> + < ?php cache_view('partials/footer', [], true) ? > + + </div> + + <!-- scripts --> + <?php link_asset('js', + ['require.files', 'require.slim', 'plugins.min', 'plugins.setup' ] + ); ?> + +</body> +</html><?php ob_flush(); ?> diff --git a/html/app/views/group.php b/html/app/views/group.php new file mode 100644 index 0000000..fddd3d2 --- /dev/null +++ b/html/app/views/group.php @@ -0,0 +1,18 @@ +<img src="https://cdn.sklavenitis.co.gr/uploads/products/1180106.jpg"> +<!-- +<img src="https://s1.sklavenitis.gr/images/1600x1600/40/files/ProductMedia/Products/1222798/1.jpg" style="max-height:240px;"> --> +<img src="https://s1.sklavenitis.gr/images/ProductDetail/40/files/ProductMedia/Products/1222798/1.jpg"> + +<section> + <div style="padding: 1em"> + + <?php foreach ($group as $item) : ?> + + <div style="padding: 1em"> + <?php render_view('item', ['item' => $item]) ?> + </div> + + <?php endforeach; ?> + + </div> +</section> diff --git a/html/app/views/item.php b/html/app/views/item.php new file mode 100644 index 0000000..2354c81 --- /dev/null +++ b/html/app/views/item.php @@ -0,0 +1,4 @@ +<h3><?=$item['title']?></h3> +<p> + <i><?=$item['info']?></i> +</p>
\ No newline at end of file diff --git a/html/app/views/products_list.php b/html/app/views/products_list.php new file mode 100644 index 0000000..1d9cf38 --- /dev/null +++ b/html/app/views/products_list.php @@ -0,0 +1,47 @@ +<style> + body { + max-width: 960px; width: 100%; margin: 0 auto; + font-family: Ubuntu, Calibri, Helevetica, sans-serif; + padding: 2em 1em; + } + td { + vertical-align: top; + padding: 2px; margin: .5em; + font-size: .82em; text-align: right; color: #666; + } + + h3 { color: #924; text-align: center; } + + .-edit { + text-align: left; font-size: 1em; + color: #357; background: #f6f6f6; + padding: .5em; + } + td span { font-size: .8em; color: #9249; } + + [contenteditable='true']:focus { border-radius: 3px; } + [contenteditable='true']:focus { + outline: none; + color: #924; + border: 2px solid #9249; border-top: 4px solid #9249; + } + +</style> + +<h3>— products —</h3> + +<?php foreach( $products as $product) : ?> + + <table> + <?php foreach ($product as $key => $val) : ?> + + <tr> + <td><?= $key ?><br/><span>int(11) NOT NULL</span></td> + <td class='-edit' id='<?=$key .'_'. $product['ID']?>' contenteditable="true"><?=$val?></td> + </tr> + + <?php endforeach ?> + </table> + <hr /> + +<?php endforeach ?> |
