diff --git a/.gitignore b/.gitignore index 9bc33e0..e257bbd 100644 --- a/.gitignore +++ b/.gitignore @@ -41,7 +41,6 @@ Temporary Items .apdisk # keep specific directories -!uploads/images/.gitignore !bin/cli/.gitignore # keep main directories @@ -56,7 +55,6 @@ Temporary Items .htaccess app/config/* !app/config/constants.php -uploads/images/* logs/* .vscode/ mail.log diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 2952fb1..0000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,74 +0,0 @@ -stages: - - prepare - - build - - test - - update - - deploy - -variables: - TIMEZONE: "America/New_York" # For the system in general - DATE_TIMEZONE: ${TIMEZONE} # For PHP - - GIT_DEPTH: 1 - GITLAB_API_URL: ${CI_API_V4_URL} - TARGET_BRANCH: ${CI_COMMIT_REF_NAME} # This is the branch chosen in the `Pipeline Schedule` - TARGET_REMOTE: "https://${GITLAB_USERNAME}:${GITLAB_ACCESS_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_NAMESPACE}/${CI_PROJECT_NAME}.git" - - # These could/should be overridden in an extending job: - UPDATE_BRANCH_PREFIX: "update_PHP_deps_" # Used for the update branch name, it will be followed by the datetime - GIT_USER: "DependBot" # Used for the update commit - GIT_EMAIL: "webmaster@thetempusproject.com" # Used for the update commit - GITLAB_USERNAME: "root" # Used for pushing the new branch and opening the MR - GITLAB_ACCESS_TOKEN: "glpat-PKEmivGtBfbz4DVPdhzk" # Used for pushing the new branch and opening the MR - MERGE_IF_SUCCESSFUL: "true" # Set to true, to merge automatically if the pipeline succeeds - SECONDS_BETWEEN_POOLING: 10 # Nbr of seconds between checking if the MR pipeline is successful, so then it will merge - JOB_GIT_FLAGS: "" - JOB_CURL_FLAGS: "" - JOB_COMPOSER_FLAGS: "" - -composer_update: - stage: update - rules: - - if: '$CI_COMMIT_BRANCH == "main"' - image: composer:latest - interruptible: true # allows to stop the job if a newer pipeline starts, saving resources and allowing new jobs to start because job concurrency is limited - script: - - git ${JOB_GIT_FLAGS} fetch origin ${TARGET_BRANCH} - - git ${JOB_GIT_FLAGS} checkout ${TARGET_BRANCH} - - git reset --hard origin/main - - git pull --allow-unrelated-histories - - export DATE_TIME="$(date '+%Y%m%d%H%M%S')" - - export MR_BRANCH="${UPDATE_BRANCH_PREFIX}${DATE_TIME}" - - git ${JOB_GIT_FLAGS} checkout -b "${MR_BRANCH}" - - composer update ${JOB_COMPOSER_FLAGS} - - if [ "$(git diff)" == "" ]; then echo "No updates needed!"; exit 0; fi - - export TITLE="Update PHP dependencies [${DATE_TIME}]" - - git ${JOB_GIT_FLAGS} commit -a -m "${TITLE}" - - git ${JOB_GIT_FLAGS} push "${TARGET_REMOTE}" "${MR_BRANCH}" - artifacts: - paths: - - vendor/ - cache: - key: ${CI_COMMIT_REF_SLUG} - paths: - - vendor/ - -prepare: - stage: prepare - script: - - echo "Preparing environment..." - -build: - stage: build - script: - - echo "Building the project..." - -test: - stage: test - script: - - echo "Running tests..." - -deploy: - stage: deploy - script: - - echo "Deploying the project..." \ No newline at end of file diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php deleted file mode 100644 index 4f6d829..0000000 --- a/.php-cs-fixer.php +++ /dev/null @@ -1,127 +0,0 @@ -exclude('somedir') - //->notPath('src/Symfony/Component/Translation/Tests/fixtures/resources.php' - ->in(__DIR__) -; - -$config = new \PhpCsFixer\Config(); -return $config->setRules([ - '@PSR2' => true, - 'array_indentation' => true, - 'array_syntax' => ['syntax' => 'short'], - 'combine_consecutive_unsets' => true, - 'class_attributes_separation' => ['elements' => ['method' => 'one',]], - 'multiline_whitespace_before_semicolons' => false, - 'single_quote' => true, - 'strict_param' => false, - 'binary_operator_spaces' => [ - 'operators' => [ - // '=>' => 'align', - // '=' => 'align' - ] - ], - // 'blank_line_after_opening_tag' => true, - // 'blank_line_before_statement' => true, - 'braces' => [ - 'allow_single_line_closure' => true, - 'position_after_functions_and_oop_constructs' => 'same' - ], - // 'cast_spaces' => true, - // 'class_definition' => array('singleLine' => true), - 'concat_space' => ['spacing' => 'one'], - // 'declare_equal_normalize' => true, - // 'function_typehint_space' => true, - // 'single_line_comment_style' => ['comment_types' => ['hash']], - // 'include' => true, - // 'lowercase_cast' => true, - // 'native_function_casing' => true, - // 'new_with_braces' => true, - // 'no_blank_lines_after_class_opening' => true, - // 'no_blank_lines_after_phpdoc' => true, - // 'no_blank_lines_before_namespace' => true, - 'no_empty_comment' => true, - 'no_empty_phpdoc' => true, - // 'no_empty_statement' => true, - 'no_extra_blank_lines' => [ - 'tokens' => [ - // 'curly_brace_block', - // 'extra', - // 'parenthesis_brace_block', - // 'square_brace_block', - // 'throw', - // 'use', - ] - ], - 'no_leading_import_slash' => true, - 'no_leading_namespace_whitespace' => true, - 'no_mixed_echo_print' => ['use' => 'echo'], - 'no_multiline_whitespace_around_double_arrow' => true, - 'no_short_bool_cast' => true, - 'no_singleline_whitespace_before_semicolons' => true, - 'no_spaces_around_offset' => ['positions' => ['outside']], - 'no_trailing_comma_in_singleline' => ['elements' => ['arguments', 'array_destructuring', 'array', 'group_import']], - - 'spaces_inside_parentheses' => ['space' => 'single'], - // 'no_spaces_inside_parenthesis' => false, - 'control_structure_braces' => true, - - 'curly_braces_position' => [ - 'control_structures_opening_brace' => 'same_line', - 'functions_opening_brace' => 'same_line', - 'classes_opening_brace' => 'same_line', - ], - - - // need to add space after array declaration - // need to put each element on a line by itself when an array is multi line - - - - - - - - - - - 'no_unneeded_control_parentheses' => true, - 'no_unused_imports' => true, - 'no_whitespace_before_comma_in_array' => true, - 'no_whitespace_in_blank_line' => true, - 'normalize_index_brace' => true, - // 'object_operator_without_whitespace' => true, - // 'php_unit_fqcn_annotation' => true, - // 'phpdoc_align' => true, - // 'phpdoc_annotation_without_dot' => true, - // 'phpdoc_indent' => true, - // 'phpdoc_inline_tag' => true, - // 'phpdoc_no_access' => true, - // 'phpdoc_no_alias_tag' => true, - // 'phpdoc_no_empty_return' => true, - // 'phpdoc_no_package' => true, - // 'phpdoc_no_useless_inheritdoc' => true, - // 'phpdoc_return_self_reference' => true, - // 'phpdoc_scalar' => true, - // 'phpdoc_separation' => true, - // 'phpdoc_single_line_var_spacing' => true, - // 'phpdoc_summary' => true, - // 'phpdoc_to_comment' => true, - // 'phpdoc_trim' => true, - // 'phpdoc_types' => true, - 'phpdoc_var_without_name' => false, - 'increment_style' => ['style' => 'post'], - 'return_type_declaration' => true, - // 'self_accessor' => true, // risky - 'short_scalar_cast' => true, - 'single_class_element_per_statement' => true, - 'standardize_not_equals' => true, - 'ternary_operator_spaces' => true, - 'trailing_comma_in_multiline' => true, - 'trim_array_spaces' => false, - 'unary_operator_spaces' => true, - 'whitespace_after_comma_in_array' => true, - 'single_blank_line_at_eof' => true - ]) - ->setLineEnding("\n") -; \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index 1227ff5..0000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,47 +0,0 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. - -## Our Standards - - -Examples of behavior that contributes to creating a positive environment include: - -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery and unwelcome sexual attention or advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at webmaster@thetempusproject.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] - -[homepage]: http://contributor-covenant.org -[version]: http://contributor-covenant.org/version/1/4/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 76d5959..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,135 +0,0 @@ -# Contribution Guidelines for TheTempusProject -Contributing to TheTempusProject is completely voluntary and should follow all of the guidelines listed here in order to ensure the highest probability of acceptance. It is highly recommended to use a php linter to automate more of this process. The project is maintained on github and all contributions need to be submitted via pull request to their specific repository under the `dev` branch. In order to contribute, simply follow the instructions for [creating a pull request](#creating-a-pull-request) below. - -## Pull Request Requirements -- All revisions must follow TTP naming conventions (see [Naming Conventions](#naming-conventions) Section) -- Include a clear and concise explanation of the features or changes included in your revision listed by file. -- All code must follow [PSR 2](http://www.php-fig.org/psr/psr-2/) standards -- prefer the use of [] for arrays over array() -- All functions must be documented with the exception of controller methods (see [Documentation](#documentation) Section) -- Controller methods may be doc-blocked when necessary for clarity (see [Documentation](#documentation) Section) -- All new Classes must include a class level doc-block (see [Documentation](#documentation) Section) -- Any new dependencies will have a longer validation process and should be accompanied by the required information (see [Dependencies](#dependencies) Section) - -## Naming Conventions -- File names are to be lower case -- All class names must be upper case -- Any data being stored as a file must be saved in the app directory (with the exception of config which should be stored under config/) -- Controllers must have a constructor and destructor using the constructor and destructor methods found in resources/ -- Views must be named using lowerCamelCase - -## Dependencies -Whenever a dependency is updated or added, pull requests must include a section that answers the following questions. -- Why is this dependency required -- Could this be reasonably accomplished within the app by implementing new features in a later version? explain. -- What is the latest stable version that can be used -- What features are absolutely necessary for your feature or modification to work - -## Documentation -### Classes - -New classes must be prefaced with a doc-block following this style: -``` -/** - * app/controllers/admin/admin.php - * - * This is the admin controller. - * - * @version 5.0.1 - * @author Joey Kimsey - * @link https://TheTempusProject.com - * @license https://opensource.org/licenses/MIT [MIT LICENSE] - */ -``` - -From top to bottom: -- Filename on the second line -- A description for the file -- The TTP version this file was built for -`@version 1.0` -- The Authors name or alias and email -`@author first last ` -- A copy of the MIT license -`@license https://opensource.org/licenses/MIT [MIT LICENSE]` -- May include a link for more information -`@link http://link.com` - -### Functions -Functions must be prefaced with a doc-block following this style: -``` -/** - * Intended as a self-destruct session. If the specified session does not - * exist, it is created. If the specified session does exist, it will be - * destroyed and returned. - * - * @param string $name - Session name to be created or checked - * @param string $string - The string to be used if session needs to be - * created. (optional) - * - * @return bool|string - Returns bool if creating, and a string if the - * check is successful. - */ -``` - -From top to bottom: -- There must be a description of the functions intended usage on the second line -- All parameters should be documented like this -`@param [type] $name - description` -- Any function with a return statement must also be documented as such -`@return [type] - description` - -## Creating a Pull Request -This is a simple explanation of how to create a pull request for changes to TheTempusProject. You can find a detailed walk-through on how to [create a pull request](https://help.github.com/articles/creating-a-pull-request/) on github. - -1. First ensure you have followed all the contributing guidelines -2. Squash your merge into a single revision. This will make it easier to view the changes as a whole. -3. You can submit a pull request [here](https://github.com/TheTempusProject/TheTempusProject/compare) -4. Please submit all pull requests to the dev branch or they will be ignored. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -add spaces after everything - -avoid "return;" just make the previous line the return - -use [] - -do not use array() - -do not use (array) - -do not add useless variables - -if you are going to set something or check if its empty, just never set it to begin with, don't set it to null \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index bb6e417..0000000 --- a/LICENSE +++ /dev/null @@ -1,25 +0,0 @@ -Copyright (c) 2024-present Joey Kimsey - -Portions of this software are licensed as follows: - -* All content residing under the "app/" directory of this repository, excluding "app/plugins/"; is licensed under "Creative Commons: CC BY-SA 4.0 license". -* All third party components incorporated into The Tempus Project Software including plugins are licensed under the original license provided by the owner of the applicable component. -* Content outside of the above mentioned directories or restrictions above is available under the "MIT Expat" license as defined below. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/README.md b/README.md deleted file mode 100644 index da6d78f..0000000 --- a/README.md +++ /dev/null @@ -1,131 +0,0 @@ -# The Tempus Project - -_Rapid Prototyping Framework built on PHP utilizing the MVC pattern with a Bootstrap front-end_ - -__Developer(s):__ - -- __Joey Kimsey__ - _Lead Developer_ - -The aim of this project is to provide a simple and stable platform from which to easily add functionality. The goal being the ability to quickly build and test new projects with a lightweight ecosystem to help. - -**Notice: This code is in _still_ not production ready. This framework is provided as is, use at your own risk.**\ -I am working very hard to ensure the system is safe and reliable enough for me to endorse its widespread use. Unfortunately, it still needs a lot of QA and improvements. - -## Table of contents - -[[_TOC_]] - -## Find Us - -* [DockerHub](https://hub.docker.com/repositories/thetempusproject) -* [Packagist](https://packagist.org/packages/thetempusproject/) -* [GitLab](https://git.thetempusproject.com/the-tempus-project/thetempusproject) - -## Summary - -The Tempus Project is a PHP application utilizing the MVC pattern to serve up simple pages and APIs with minimal effort. It requires a MySQL database to function and is designed to run equally well with nginx or apache powering the webserver. Most of the core functionality is developed in house and provided through dependencies. At this time, the frontend is driven on bootstrap 3 and FontAwesome for simplicity. - -## Features - -- A Plugin system that allows plug-and-play functionality -- A User management system - - groups - - permissions - - preferences - - registration and recovery - (All Controlled dynamically via our plugin interface) -- Compatibility with both Apache and NGINX -- Built with Bootstrap with a focus on mobile compatibility -- Incredibly easy to set-up, deploy, and develop - -## Installation - -The preferred method for installation is [Composer](#composer) but special attention has been given to installation and usage [without Composer](#composer). - -### Composer - -The simplest method to start a new project is to use composer to create a new project and automatically clone all the necessary files: - -#### via create-project - -``` -composer create-project thetempusproject/thetempusproject test-app -``` - -#### via clone & install - -1. Clone the directory to wherever you want to install the framework. -`git clone https://git.thetempusproject.com/the-tempus-project/thetempusproject.git ` -1. Open your terminal to the directory you previously cloned the repository. -`cd ` -1. Install using composer: -`php composer.phar install` - -### Manually - -1. Clone the directory to wherever you want to install the framework. -`git clone https://git.thetempusproject.com/the-tempus-project/thetempusproject.git ` -1. Open your terminal to the directory you previously cloned the repository. -`cd /` -1. Clone the dependency directories to the vendor/ folder. -``` -cd vendor/ -git clone https://git.thetempusproject.com/the-tempus-project/bedrock.git bedrock -git clone https://git.thetempusproject.com/the-tempus-project/canary.git canary -git clone https://git.thetempusproject.com/the-tempus-project/hermes.git hermes -git clone https://git.thetempusproject.com/the-tempus-project/houdini.git houdini -``` - -__Note:__ The autoloader should automatically detect and use the dependencies, but they need to be sorted into the folders ans shown above. - - -## Docker - -To enable quick deployment and collaboration The Tempus Project is distributed with the files to build your own docker images or stack with apache or nginx The included `docker-compose.yml` will load up an entire stack including apache and nginx, as well as a MySQL server with phpmyadmin. - -You will need docker installed on your system then you can either download the latest images from DockerHud: - -``` -docker pull thetempusproject/ttp-apache -docker pull thetempusproject/ttp-nginx -``` - -Or you can build your own images from this repository. More information can be found in the included README files: - -* [Apache Image](docker/ttp-apache/README.md) -* [Nginx Image](docker/ttp-nginx/README.md) - -### Docker-Compose - -The Docker stack included here will build new versions of the nginx and apache webserver and launch them in individual containers. It will also create 2 more containers; one for php, and one for phpmyadmin. - -``` -docker-compose -f docker-compose.yml up --build -d --no-cache -``` - -__Note:__ If you cloned the repository from git, you will need to copy the `docker/.env.example` to `.env` in the root directory and update the contents before proceeding with docker-compose. - -## Contributing - -TheTempusProject is an open source project and welcomes community contributions. Please refer to the [Contributing file](CONTRIBUTING.md) for more details. - -## License - -See the [LICENSE](LICENSE) file for licensing information as it pertains to files in this repository. - -## Known Issues - -- [ ] The blog plugin should add a welcome post during the installResources step of the installer. It doesn't work right now. - -## Currently being developed - -- [ ] Adding documentation -- [ ] Unit testing - -## Future updates - -- [ ] Expansion of PDO to allow different database types -- [ ] Update installer to account for database deltas, allowing easy updating. -- [ ] Implement uniformity in terms of error reporting, exceptions, logging. -- [ ] I want to make an api that allows you to download and install new plugins from a centralized repository -- [ ] i want plugin instalation to be compatible with composer for easier management of added plugins. diff --git a/app/config/constants.php b/app/config/constants.php index ab609ba..a51f251 100644 --- a/app/config/constants.php +++ b/app/config/constants.php @@ -38,8 +38,8 @@ if ( ! defined( 'CONFIG_DIRECTORY' ) ) { # Tempus Debugger define( 'CANARY_SECURE_HASH', 'd73ed7591a30f0ca7d686a0e780f0d05' ); # Tempus Project Core -define( 'APP_NAME', 'The Tempus Project'); -define( 'TP_DEFAULT_LOGO', 'images/logoWhite.png'); +define( 'APP_NAME', 'All The Bookmarks'); +define( 'TP_DEFAULT_LOGO', 'images/logo180.png'); // Check define( 'MINIMUM_PHP_VERSION', 8.1); // Cookies diff --git a/app/controllers/home.php b/app/controllers/home.php index cbbc863..2fd4a35 100644 --- a/app/controllers/home.php +++ b/app/controllers/home.php @@ -27,7 +27,10 @@ use TheTempusProject\TheTempusProject as App; class Home extends Controller { public function index() { self::$title = '{SITENAME}'; - self::$pageDescription = '{SITENAME} is here to provide you a better, faster, and easier - way to create and manage your own web applications.'; + self::$pageDescription = 'AllTheBookmarks.com is here to provide you a better, faster, and easier - way to manage bookmarks across multiple browsers and devices seamlessly.'; + if ( App::$isLoggedIn ) { + return Views::view( 'bookmarks.introduction' ); + } Views::view( 'index' ); } diff --git a/app/css/main-dark.css b/app/css/main-dark.css index f2e8eb0..0623766 100644 --- a/app/css/main-dark.css +++ b/app/css/main-dark.css @@ -33,18 +33,14 @@ color: #fff; } - - .bg-default { background-color: #2c2c2c; } + hr { color: #f5f5f5; } - - - .bg-none,.bg-warning { color: #000 !important; } diff --git a/app/css/main.css b/app/css/main.css index 6651339..500f016 100644 --- a/app/css/main.css +++ b/app/css/main.css @@ -33,7 +33,6 @@ color: #000; } - .nav-link.active { font-weight: bold; /* Make the text bold */ } @@ -323,3 +322,78 @@ body { background: linear-gradient(to right, #2c2c2c, #1e1e1e, #1e1e1e); } +.atb-green { + color: #85bd3e; +} +a.atb-green:hover { + color: #1b947f; +} +button.atb-green:hover { + color: #1b947f; +} +a.atb-green.active { + color: #1b947f !important; + border-color: #1b947f; +} + + + +.atb-green-outline-only { + border-color: #85bd3e; +} +a.atb-green-outline-only:hover { + border-color: #85bd3e; +} +button.atb-green-outline-only:hover { + border-color: #85bd3e; +} + + + +.atb-green-outline { + color: #85bd3e; + border-color: #85bd3e; +} +a.atb-green-outline:hover { + color: #1b947f; + border-color: #1b947f; +} +button.atb-green-outline:hover { + color: #1b947f; + border-color: #1b947f; +} + + +.atb-green-bg { + color: #fff; + background-color: #85bd3e; + border-color: #85bd3e; +} +a.atb-green-bg:hover { + background-color: #1b947f; + border-color: #1b947f; +} +button.atb-green-bg:hover { + background-color: #1b947f; + border-color: #1b947f; +} + + + +.bookmark-card.dragging { + opacity: 0.7; + cursor: move; /* Show a move cursor when dragging */ + } + + +.form-switch .form-check-input { + border-color: #1b947f; +} +.form-switch .form-check-input:checked { + border-color: #1b947f; + background-color: #85bd3e; +} +.form-switch .form-check-input:focus { + border-color: #1b947f; + box-shadow: #85bd3e; +} \ No newline at end of file diff --git a/app/images/clean-simple.png b/app/images/clean-simple.png new file mode 100644 index 0000000..9e23d99 Binary files /dev/null and b/app/images/clean-simple.png differ diff --git a/app/images/in-one-place.png b/app/images/in-one-place.png new file mode 100644 index 0000000..d8b8591 Binary files /dev/null and b/app/images/in-one-place.png differ diff --git a/app/images/keep-track.png b/app/images/keep-track.png new file mode 100644 index 0000000..1572861 Binary files /dev/null and b/app/images/keep-track.png differ diff --git a/app/plugins/blog/views/list.html b/app/plugins/blog/views/list.html index 89a6c9e..d1c0a1b 100644 --- a/app/plugins/blog/views/list.html +++ b/app/plugins/blog/views/list.html @@ -1,7 +1,7 @@ {LOOP}

{title}

- +
{contentSummary}
diff --git a/app/plugins/blog/views/post.html b/app/plugins/blog/views/post.html index e470cff..e06fd84 100644 --- a/app/plugins/blog/views/post.html +++ b/app/plugins/blog/views/post.html @@ -3,7 +3,7 @@

{title}


- + {content} {ADMIN}
diff --git a/app/plugins/blog/views/widgets/archive.html b/app/plugins/blog/views/widgets/archive.html index 63fb52d..40b42c1 100644 --- a/app/plugins/blog/views/widgets/archive.html +++ b/app/plugins/blog/views/widgets/archive.html @@ -6,7 +6,7 @@
    {LOOP} -
  1. ({count}) {monthText} {year}
  2. +
  3. ({count}) {monthText} {year}
  4. {/LOOP} {ALT}
  5. None To Show
  6. diff --git a/app/plugins/bookmarks/controllers/api/bookmark_folders.php b/app/plugins/bookmarks/controllers/api/bookmark_folders.php new file mode 100644 index 0000000..b76749a --- /dev/null +++ b/app/plugins/bookmarks/controllers/api/bookmark_folders.php @@ -0,0 +1,68 @@ + + * @link https://TheTempusProject.com + * @license https://opensource.org/licenses/MIT [MIT LICENSE] + */ +namespace TheTempusProject\Controllers\Api; + +use TheTempusProject\Models\User; +use TheTempusProject\Classes\ApiController; +use TheTempusProject\Houdini\Classes\Views; +use TheTempusProject\Canary\Bin\Canary as Debug; +use TheTempusProject\Bedrock\Functions\Input; +use TheTempusProject\Models\Folders; + +class BookmarkFolders extends ApiController { + protected static $folders; + + public function __construct() { + header('Access-Control-Allow-Origin: *'); + parent::__construct(); + self::$folders = new Folders; + } + + public function create() { + $user = self::$authToken->createdBy; + + $payload = @file_get_contents('php://input'); + $payload = json_decode( $payload, true ); + + $result = self::$folders->create( + $payload['name'], + $payload['folder'] ?? 0, + $payload['notes'] ?? '', + $payload['color'] ?? 'default', + $payload['privacy'] ?? 'private', + $user + ); + + if ( ! $result ) { + $responseType = 'error'; + $response = 'There was an error creating your folder.'; + } else { + $responseType = 'id'; + $response = $result; + } + Views::view( 'api.response', ['response' => json_encode( [ $responseType => $response ], true )]); + } + + public function list( $id = '' ) { + $user = self::$authToken->createdBy; + $folders = self::$folders->bySpecificUser( $user ); + + if ( ! $folders ) { + $responseType = 'error'; + $response = 'There was an error creating your folder.'; + } else { + $responseType = 'folders'; + $response = $folders; + } + Views::view( 'api.response', ['response' => json_encode( [ $responseType => $response ], true )]); + } +} diff --git a/app/plugins/bookmarks/controllers/api/bookmarks.php b/app/plugins/bookmarks/controllers/api/bookmarks.php new file mode 100644 index 0000000..1903f78 --- /dev/null +++ b/app/plugins/bookmarks/controllers/api/bookmarks.php @@ -0,0 +1,56 @@ + + * @link https://TheTempusProject.com + * @license https://opensource.org/licenses/MIT [MIT LICENSE] + */ +namespace TheTempusProject\Controllers\Api; + +use TheTempusProject\Models\User; +use TheTempusProject\Classes\ApiController; +use TheTempusProject\Houdini\Classes\Views; +use TheTempusProject\Canary\Bin\Canary as Debug; +use TheTempusProject\Bedrock\Functions\Input; +use TheTempusProject\Models\Bookmarks as Bookmark; + +class Bookmarks extends ApiController { + protected static $bookmarks; + + public function __construct() { + parent::__construct(); + self::$bookmarks = new Bookmark; + } + + public function create() { + header('Access-Control-Allow-Origin: *'); + + $user = self::$authToken->createdBy; + + $payload = @file_get_contents('php://input'); + $payload = json_decode( $payload, true ); + + $result = self::$bookmarks->create( + $payload['name'], + $payload['url'], + $payload['folder'] ?? 0, + $payload['notes'] ?? '', + $payload['color'] ?? 'default', + $payload['privacy'] ?? 'private', + 'external', + $user + ); + if ( ! $result ) { + $responseType = 'error'; + $response = 'There was an error creating your folder.'; + } else { + $responseType = 'data'; + $response = $result; + } + Views::view( 'api.response', ['response' => json_encode( [ $responseType => $response ], true )]); + } +} \ No newline at end of file diff --git a/app/plugins/bookmarks/controllers/bookmarks.php b/app/plugins/bookmarks/controllers/bookmarks.php new file mode 100644 index 0000000..434bb37 --- /dev/null +++ b/app/plugins/bookmarks/controllers/bookmarks.php @@ -0,0 +1,992 @@ + + * @link https://TheTempusProject.com + * @license https://opensource.org/licenses/MIT [MIT LICENSE] + */ +namespace TheTempusProject\Controllers; + +use TheTempusProject\Hermes\Functions\Redirect; +use TheTempusProject\Bedrock\Functions\Check; +use TheTempusProject\Bedrock\Functions\Input; +use TheTempusProject\Bedrock\Functions\Session; +use TheTempusProject\Houdini\Classes\Issues; +use TheTempusProject\Houdini\Classes\Views; +use TheTempusProject\Classes\Controller; +use TheTempusProject\Classes\Forms; +use TheTempusProject\Models\Bookmarks as Bookmark; +use TheTempusProject\Models\Folders; +use TheTempusProject\Models\BookmarkDashboards as Dashboards; +use TheTempusProject\TheTempusProject as App; +use TheTempusProject\Houdini\Classes\Components; +use TheTempusProject\Houdini\Classes\Forms as HoudiniForms; +use TheTempusProject\Houdini\Classes\Navigation; +use TheTempusProject\Houdini\Classes\Template; +use TheTempusProject\Hermes\Functions\Route as Routes; +use TheTempusProject\Models\User; +use TheTempusProject\Classes\Preferences; +use TheTempusProject\Canary\Bin\Canary as Debug; + +class Bookmarks extends Controller { + protected static $bookmarks; + protected static $folders; + protected static $dashboards; + + public function __construct() { + parent::__construct(); + if ( ! App::$isLoggedIn ) { + Session::flash( 'notice', 'You must be logged in to create or manage bookmarks.' ); + return Redirect::home(); + } + self::$bookmarks = new Bookmark; + self::$folders = new Folders; + self::$dashboards = new Dashboards; + self::$title = 'Bookmarks - {SITENAME}'; + self::$pageDescription = 'Add and save url bookmarks here.'; + + $folderTabs = Views::simpleView( 'bookmarks.nav.folderTabs' ); + if ( stripos( Input::get('url'), 'bookmarks/bookmarks' ) !== false ) { + $tabsView = Navigation::activePageSelect( $folderTabs, '/bookmarks/folders/', false, true ); + $userFolderTabs = Views::simpleView('bookmarks.nav.userFolderTabs', self::$folders->simpleObjectByUser(true) ); + $userFolderTabsView = Navigation::activePageSelect( $userFolderTabs, Input::get( 'url' ), false, true ); + } else { + $tabsView = Navigation::activePageSelect( $folderTabs, Input::get( 'url' ), false, true ); + $userFolderTabsView = ''; + } + Components::set( 'userFolderTabs', $userFolderTabsView ); + Components::set( 'SITE_URL', Routes::getAddress() ); + Views::raw( $tabsView ); + Components::append( 'TEMPLATE_JS_INCLUDES', Template::parse('' ) ); + $viewOptions = Views::simpleView( 'bookmarks.nav.viewOptions' ); + Components::set( 'VIEW_OPTIONS', $viewOptions ); + $dashOptions = Views::simpleView( 'bookmarks.dashboards.dashOptions' ); + Components::set( 'DASH_OPTIONS', $dashOptions ); + $this->setPrefToggles(); + } + + public function index() { + self::$title = 'Manage Bookmarks - {SITENAME}'; + if ( Input::exists('submit') ) { + $prefs = new Preferences; + $user = new User; + $fields = $prefs->convertFormToArray( true ); + $out = $user->updatePrefs( $fields, App::$activeUser->ID ); + $this->setPrefToggles(); + } + + $folders = self::$folders->byUser(); + $panelArray = []; + if ( !empty( $folders ) ) { + foreach ( $folders as $folder ) { + $panel = new \stdClass(); + $folderObject = new \stdClass(); + $folderObject->bookmarks = self::$bookmarks->byFolder( $folder->ID ); + $folderObject->ID = $folder->ID; + $folderObject->title = $folder->title; + $folderObject->color = $folder->color; + $folderObject->uuid = $folder->uuid; + $folderObject->bookmarkListRows = Views::simpleView( 'bookmarks.components.bookmarkListRows', $folderObject->bookmarks ); + $panelArray[] = $folderObject; + } + } + if ( ! empty( $folders ) ) { + Components::set( 'folderPanels', Views::simpleView( 'bookmarks.components.bookmarkListPanel', $panelArray ) ); + return Views::view( 'bookmarks.dash' ); + } + return Views::view( 'bookmarks.indexExplainer' ); + } + + /** + * Bookmarks + */ + public function bookmark( $id = 0 ) { + $bookmark = self::$bookmarks->findById( $id ); + if ( $bookmark == false ) { + Session::flash( 'error', 'Bookmark not found.' ); + return Redirect::to( 'bookmarks/index' ); + } + if ( $bookmark->createdBy != App::$activeUser->ID ) { + Session::flash( 'error', 'You do not have permission to modify this bookmark.' ); + return Redirect::to( 'bookmarks/index' ); + } + Navigation::setCrumbComponent( 'BookmarkBreadCrumbs', 'bookmarks/bookmark/' . $id ); + return Views::view( 'bookmarks.bookmarks.view', $bookmark ); + } + + public function unsorted() { + self::$title = 'Unsorted Bookmarks - {SITENAME}'; + $bookmarks = self::$bookmarks->noFolder(); + Views::view( 'bookmarks.bookmarks.unsorted', $bookmarks ); + } + + public function bookmarks( $id = null ) { + $folder = self::$folders->findById( $id ); + if ( $folder == false ) { + Session::flash( 'error', 'Folder not found.' ); + return Redirect::to( 'bookmarks/index' ); + } + if ( $folder->createdBy != App::$activeUser->ID ) { + Session::flash( 'error', 'You do not have permission to view this folder.' ); + return Redirect::to( 'bookmarks/index' ); + } + Navigation::setCrumbComponent( 'BookmarkBreadCrumbs', 'bookmarks/bookmarks/' . $id ); + + $panelArray = []; + $panel = new \stdClass(); + $folderObject = new \stdClass(); + $folderObject->bookmarks = self::$bookmarks->byFolder( $folder->ID ); + $folderObject->ID = $folder->ID; + $folderObject->title = $folder->title; + $folderObject->color = $folder->color; + $folderObject->bookmarkListRows = Views::simpleView( 'bookmarks.components.bookmarkListRows', $folderObject->bookmarks ); + $panel->panel = Views::simpleView( 'bookmarks.components.bookmarkListPanel', [$folderObject] ); + $panelArray[] = $panel; + + return Views::view( 'bookmarks.bookmarks.listPage', $panelArray ); + } + + public function createBookmark( $id = null ) { + self::$title = 'Add Bookmark - {SITENAME}'; + $folderID = Input::get('folder_id') ? Input::get('folder_id') : $id; + $folderID = Input::post('folder_id') ? Input::post('folder_id') : $id; + $this->setFolderSelect( $folderID ); + + if ( ! Input::exists() ) { + return Views::view( 'bookmarks.bookmarks.create' ); + } + if ( ! Forms::check( 'createBookmark' ) ) { + Issues::add( 'error', [ 'There was an error with your form.' => Check::userErrors() ] ); + return Views::view( 'bookmarks.bookmarks.create' ); + } + + $result = self::$bookmarks->create( + Input::post('title'), + Input::post('url'), + $folderID, + Input::post('description'), + Input::post('color'), + Input::post('privacy'), + ); + + if ( ! $result ) { + Issues::add( 'error', [ 'There was an error creating your bookmark.' => Check::userErrors() ] ); + return Views::view( 'bookmarks.bookmarks.create' ); + } + // self::$bookmarks->refreshInfo( $result ); + Session::flash( 'success', 'Your Bookmark has been created.' ); + if ( ! empty( $folderID ) ) { + Redirect::to( 'bookmarks/bookmarks/'. $folderID ); + } else { + Redirect::to( 'bookmarks/index' ); + } + } + + public function editBookmark( $id = null ) { + self::$title = 'Edit Bookmark - {SITENAME}'; + $folderID = Input::exists('folder_id') ? Input::post('folder_id') : ''; + + $bookmark = self::$bookmarks->findById( $id ); + if ( $bookmark == false ) { + Issues::add( 'error', 'Bookmark not found.' ); + return Redirect::to( 'bookmarks/index' ); + } + if ( $bookmark->createdBy != App::$activeUser->ID ) { + Issues::add( 'error', 'You do not have permission to modify this bookmark.' ); + return Redirect::to( 'bookmarks/index' ); + } + if ( empty( $folderID ) ) { + $folderID = $bookmark->folderID; + } + + $this->setFolderSelect( $folderID ); + Components::set( 'color', $bookmark->color ); + + if ( ! Input::exists( 'submit' ) ) { + return Views::view( 'bookmarks.bookmarks.edit', $bookmark ); + } + if ( ! Forms::check( 'editBookmark' ) ) { + Issues::add( 'error', [ 'There was an error updating your bookmark.' => Check::userErrors() ] ); + return Views::view( 'bookmarks.bookmarks.edit', $bookmark ); + } + + $result = self::$bookmarks->update( + $id, + Input::post('title'), + Input::post('url'), + $folderID, + Input::post('description'), + Input::post('color'), + Input::post('privacy'), + ); + if ( ! $result ) { + Issues::add( 'error', [ 'There was an error updating your bookmark.' => Check::userErrors() ] ); + return Views::view( 'bookmarks.bookmarks.edit', $bookmark ); + } + Session::flash( 'success', 'Your Bookmark has been updated.' ); + Redirect::to( 'bookmarks/folders/'. $bookmark->folderID ); + } + + public function deleteBookmark( $id = null ) { + $bookmark = self::$bookmarks->findById( $id ); + if ( $bookmark == false ) { + Issues::add( 'error', 'Bookmark not found.' ); + return $this->index(); + } + if ( $bookmark->createdBy != App::$activeUser->ID ) { + Issues::add( 'error', 'You do not have permission to modify this bookmark.' ); + return $this->index(); + } + $result = self::$bookmarks->delete( $id ); + if ( !$result ) { + Session::flash( 'error', 'There was an error deleting the bookmark(s)' ); + } else { + Session::flash( 'success', 'Bookmark deleted' ); + } + Redirect::to( 'bookmarks/folders/'. $bookmark->folderID ); + } + + /** + * Folders + */ + public function folders( $id = null) { + $folder = self::$folders->findById( $id ); + if ( $folder == false ) { + $folders = self::$folders->byUser(); + Components::set( 'foldersList', Views::simpleView( 'bookmarks.folders.list', $folders ) ); + return Views::view( 'bookmarks.folders.listPage' ); + } + if ( $folder->createdBy != App::$activeUser->ID ) { + Session::flash( 'error', 'You do not have permission to view this folder.' ); + return Redirect::to( 'bookmarks/index' ); + } + Navigation::setCrumbComponent( 'BookmarkBreadCrumbs', 'bookmarks/folders/' . $id ); + return Views::view( 'bookmarks.folders.view', $folder ); + } + + public function createFolder( $id = 0 ) { + self::$title = 'Create Folder - {SITENAME}'; + $folderID = Input::exists('folder_id') ? Input::post('folder_id') : $id; + $folders = self::$folders->simpleByUser(); + if ( ! empty( $folders ) ) { + $this->setFolderSelect( $folderID ); + } else { + $folderSelect = ''; + Components::set( 'folderSelect', $folderSelect ); + } + if ( ! Input::exists() ) { + return Views::view( 'bookmarks.folders.create' ); + } + if ( ! Forms::check( 'createFolder' ) ) { + Issues::add( 'error', [ 'There was an error creating your folder.' => Check::userErrors() ] ); + return Views::view( 'bookmarks.folders.create' ); + } + $folder = self::$folders->create( Input::post('title'), $folderID, Input::post('description'), Input::post('color'), Input::post('privacy') ); + if ( ! $folder ) { + return Views::view( 'bookmarks.folders.create' ); + } + Session::flash( 'success', 'Your Folder has been created.' ); + Redirect::to( 'bookmarks/folders' ); + } + + public function editFolder( $id = null ) { + self::$title = 'Edit Folder - {SITENAME}'; + $folder = self::$folders->findById( $id ); + + if ( $folder == false ) { + Issues::add( 'error', 'Folder not found.' ); + return $this->index(); + } + + if ( $folder->createdBy != App::$activeUser->ID ) { + Issues::add( 'error', 'You do not have permission to modify this folder.' ); + return $this->index(); + } + $folderID = ( false === Input::exists('folder_id') ) ? $folder->ID : Input::post('folder_id'); + + $this->setFolderSelect( $folderID ); + Components::set( 'color', $folder->color ); + + if ( ! Input::exists( 'submit' ) ) { + return Views::view( 'bookmarks.folders.edit', $folder ); + } + + if ( !Forms::check( 'editFolder' ) ) { + Issues::add( 'error', [ 'There was an error editing your folder.' => Check::userErrors() ] ); + return Views::view( 'bookmarks.folders.edit', $folder ); + } + + $result = self::$folders->update( $id, Input::post('title'), $folderID, Input::post('description'), Input::post('color'), Input::post('privacy') ); + if ( !$result ) { + Issues::add( 'error', [ 'There was an error updating your folder.' => Check::userErrors() ] ); + return Views::view( 'bookmarks.folders.edit', $folder ); + } + Session::flash( 'success', 'Your Folder has been updated.' ); + Redirect::to( 'bookmarks/folders/'. $folder->ID ); + } + + public function deleteFolder( $id = null ) { + $folder = self::$folders->findById( $id ); + if ( $folder == false ) { + Issues::add( 'error', 'Folder not found.' ); + return $this->index(); + } + if ( $folder->createdBy != App::$activeUser->ID ) { + Issues::add( 'error', 'You do not have permission to modify this folder.' ); + return $this->index(); + } + $results = self::$bookmarks->deleteByFolder( $id ); + $result = self::$folders->delete( $id ); + if ( !$result ) { + Session::flash( 'error', 'There was an error deleting the folder(s)' ); + } else { + Session::flash( 'success', 'Folder deleted' ); + } + Redirect::to( 'bookmarks/folders' ); + } + + /** + * Dashboards + */ + public function addDash() { + self::$title = 'Add Dashboard - {SITENAME}'; + if ( !App::$isMember ) { + Issues::add( 'notice', 'You must have an active membership to add dashboards.' ); + return $this->index(); + } + $folders = self::$folders->byUser() ?? []; + + if ( !empty( $folders ) ) { + foreach ( $folders as &$folder ) { + $folder->selected = ''; + } + } + + $linkSelect = Views::simpleView( 'bookmarks.components.linkSelect', $folders ); + Components::set( 'LINK_SELECT', $linkSelect ); + + if ( ! Input::exists( 'submit' ) ) { + return Views::view( 'bookmarks.dashboards.create' ); + } + + if ( !Forms::check( 'createDashboard' ) ) { + Issues::add( 'error', [ 'There was an error creating your dashboard.' => Check::userErrors() ] ); + return Views::view( 'bookmarks.dashboards.create' ); + } + + if ( is_array( Input::post('link_filter') ) && ! empty( Input::post('link_filter') ) ) { + $filters = implode( ',', Input::post('link_filter') ); + } else { + $filters = ''; + } + if ( is_array( Input::post('link_order') ) && ! empty( Input::post('link_order') ) ) { + $folders = implode( ',', Input::post('link_order') ); + } else { + $folders = ''; + } + $result = self::$dashboards->create( Input::post('title'), $filters, $folders, Input::post('description') ); + + if ( !$result ) { + Issues::add( 'error', [ 'There was an error creating your dashboard.' => Check::userErrors() ] ); + return Views::view( 'bookmarks.dashboards.create' ); + } + + Issues::add( 'success', 'Your dashboard has been created.' ); + return $this->dashboards(); + } + + public function editDash( $id = null ) { + self::$title = 'Edit Dashboard - {SITENAME}'; + if ( !App::$isMember ) { + Issues::add( 'notice', 'You must have an active membership to edit dashboards.' ); + return $this->index(); + } + $dash = self::$dashboards->findById( $id ); + + if ( $dash == false ) { + Issues::add( 'error', 'Unknown Dashboard' ); + return $this->dashboards(); + } + + if ( $dash->createdBy != App::$activeUser->ID ) { + Issues::add( 'error', 'You do not have permission to view this dashboard.' ); + return $this->dashboards(); + } + + $this->setDashToggles( explode( ',', $dash->saved_prefs ) ); + + $folders = self::$folders->byUser() ?? []; + $selectedFolders = explode( ',', $dash->link_order ); + if ( !empty( $folders ) ) { + foreach ( $folders as &$folder ) { + if ( in_array( $folder->ID, $selectedFolders ) ) { + $folder->selected = ' checked'; + } else { + $folder->selected = ''; + } + } + } + + $linkSelect = Views::simpleView( 'bookmarks.components.linkSelect', $folders ); + Components::set( 'LINK_SELECT', $linkSelect ); + + if ( ! Input::exists( 'submit' ) ) { + return Views::view( 'bookmarks.dashboards.edit', $dash ); + } + + if ( ! Forms::check( 'editDashboard' ) ) { + Issues::add( 'error', [ 'There was an error editing your dashboard.' => Check::userErrors() ] ); + return Views::view( 'bookmarks.dashboards.edit', $dash ); + } + + if ( is_array( Input::post('link_filter') ) && ! empty( Input::post('link_filter') ) ) { + $filters = implode( ',', Input::post('link_filter') ); + } else { + $filters = ''; + } + + if ( is_array( Input::post('link_order') ) && ! empty( Input::post('link_order') ) ) { + $folders = implode( ',', Input::post('link_order') ); + } else { + $folders = ''; + } + + $result = self::$dashboards->update( $id, Input::post('title'), $filters, $folders, Input::post('description') ); + + if ( !$result ) { + Issues::add( 'error', [ 'There was an error updating your dashboard.' => Check::userErrors() ] ); + return Views::view( 'bookmarks.dashboards.edit', $dash ); + } + + Issues::add( 'success', 'Your dashboard has been updated.' ); + return $this->dashboards(); + } + + public function deleteDash( $id = null ) { + if ( !App::$isMember ) { + Issues::add( 'notice', 'You must have an active membership to delete dashboards.' ); + return $this->index(); + } + $dash = self::$dashboards->findById( $id ); + if ( $dash == false ) { + Issues::add( 'error', 'Unknown Dashboard' ); + return $this->dashboards(); + } + if ( $dash->createdBy != App::$activeUser->ID ) { + Issues::add( 'error', 'You do not have permission to delete this dash.' ); + return $this->dashboards(); + } + $result = self::$dashboards->delete( $id ); + if ( !$result ) { + Issues::add( 'error', 'There was an error deleting the dashboard(s)' ); + } else { + Issues::add( 'success', 'Dashboard deleted' ); + } + return $this->dashboards(); + } + + public function dashboard( $uuid = null ) { + self::$title = 'Bookmark Dashboard - {SITENAME}'; + if ( !App::$isMember ) { + Issues::add( 'notice', 'You must have an active membership to view dashboards.' ); + return $this->index(); + } + $dash = self::$dashboards->findByUuid( $uuid ); + if ( $dash == false ) { + return $this->dashboards(); + } + if ( $dash->createdBy != App::$activeUser->ID ) { + Issues::add( 'error', 'You do not have permission to view this dash.' ); + return $this->dashboards(); + } + if ( Input::exists( 'submit' ) ) { + if ( Forms::check( 'updateDashboard' ) ) { + $filters = ''; + $folders = ''; + + if ( is_array( Input::post('link_filter') ) && ! empty( Input::post('link_filter') ) ) { + $filters = implode( ',', Input::post('link_filter') ); + } + + if ( ! empty( Input::post('link_order') ) ) { + $folders = Input::post('link_order'); + } + + $result = self::$dashboards->updateDash( $dash->ID, $filters, $folders ); + + if ( !$result ) { + Issues::add( 'error', [ 'There was an error saving your dashboard.' => Check::userErrors() ] ); + } else { + Issues::add( 'success', 'Your dashboard has been saved.' ); + } + } else { + Issues::add( 'error', [ 'There was an error saving your dashboard.' => Check::userErrors() ] ); + } + unset( $_POST ); + } + $dash = self::$dashboards->findByUuid( $uuid ); + + $foldersArray = []; + if ( ! empty( $dash->link_order ) ) { + $folders = explode( ',', $dash->link_order ); + foreach ( $folders as $key => $id ) { + $folder = self::$folders->findById( $id ); + if ( empty( $folder ) ) { + continue; + } + + $bookmarks = self::$bookmarks->byFolder( $folder->ID ); + if ( empty( $bookmarks ) ) { + continue; + } + + $folderObject = new \stdClass(); + $folderObject->ID = $folder->ID; + $folderObject->title = $folder->title; + $folderObject->color = $folder->color; + $folderObject->uuid = $folder->uuid; + $folderObject->bookmarkRows = Views::simpleView( 'bookmarks.dashboards.bookmarkRows', $bookmarks ); + $foldersArray[] = $folderObject; + } + } + Components::set( 'folderPanels', Views::simpleView( 'bookmarks.dashboards.folderPanels', $foldersArray ) ); + + if ( ! empty( $dash->saved_prefs ) ) { + $this->setDashToggles( explode( ',', $dash->saved_prefs ) ); + } else { + $this->setDashToggles( [] ); + } + + return Views::view( 'bookmarks.dashboards.view', $dash ); + } + + public function dashboards() { + self::$title = 'Bookmark Dashboards - {SITENAME}'; + + if ( !App::$isMember ) { + Issues::add( 'notice', 'You will need an active subscription to start creating dashboards. You can check our Pricing page for more details.' ); + return Views::view( 'bookmarks.dashboardExplainer' ); + } + $dashboards = self::$dashboards->byUser(); + return Views::view( 'bookmarks.dashboards.list', $dashboards ); + } + + /** + * Functionality + */ + public function publish( $id = null ) { + $bookmark = self::$bookmarks->findById( $id ); + if ( $bookmark == false ) { + Session::flash( 'error', 'Bookmark not found.' ); + return Redirect::to( 'bookmarks/index' ); + } + if ( $bookmark->createdBy != App::$activeUser->ID ) { + Session::flash( 'error', 'You do not have permission to modify this bookmark.' ); + return Redirect::to( 'bookmarks/index' ); + } + self::$bookmarks->publish( $id ); + Session::flash( 'success', 'Bookmark mad Public.' ); + return Redirect::to( 'bookmarks/share' ); + } + + public function retract( $id = null ) { + $bookmark = self::$bookmarks->findById( $id ); + if ( $bookmark == false ) { + Session::flash( 'error', 'Bookmark not found.' ); + return Redirect::to( 'bookmarks/index' ); + } + if ( $bookmark->createdBy != App::$activeUser->ID ) { + Session::flash( 'error', 'You do not have permission to modify this bookmark.' ); + return Redirect::to( 'bookmarks/index' ); + } + self::$bookmarks->retract( $id ); + Session::flash( 'success', 'Bookmark made Private.' ); + return Redirect::to( 'bookmarks/share' ); + } + + public function hideBookmark( $id = null ) { + $bookmark = self::$bookmarks->findById( $id ); + if ( $bookmark == false ) { + Session::flash( 'error', 'Bookmark not found.' ); + return Redirect::to( 'bookmarks/index' ); + } + if ( $bookmark->createdBy != App::$activeUser->ID ) { + Session::flash( 'error', 'You do not have permission to modify this bookmark.' ); + return Redirect::to( 'bookmarks/index' ); + } + self::$bookmarks->hide( $id ); + Session::flash( 'success', 'Bookmark hidden.' ); + return Redirect::to( 'bookmarks/index' ); + } + + public function archiveBookmark( $id = null ) { + $bookmark = self::$bookmarks->findById( $id ); + if ( $bookmark == false ) { + Session::flash( 'error', 'Bookmark not found.' ); + return Redirect::to( 'bookmarks/index' ); + } + if ( $bookmark->createdBy != App::$activeUser->ID ) { + Session::flash( 'error', 'You do not have permission to modify this bookmark.' ); + return Redirect::to( 'bookmarks/index' ); + } + self::$bookmarks->archive( $id ); + Session::flash( 'success', 'Bookmark archived.' ); + return Redirect::to( 'bookmarks/index' ); + } + + public function showBookmark( $id = null ) { + $bookmark = self::$bookmarks->findById( $id ); + if ( $bookmark == false ) { + Session::flash( 'error', 'Bookmark not found.' ); + return Redirect::to( 'bookmarks/index' ); + } + if ( $bookmark->createdBy != App::$activeUser->ID ) { + Session::flash( 'error', 'You do not have permission to modify this bookmark.' ); + return Redirect::to( 'bookmarks/index' ); + } + self::$bookmarks->show( $id ); + Session::flash( 'success', 'Bookmark shown.' ); + return Redirect::to( 'bookmarks/index' ); + } + + public function unarchiveBookmark( $id = null ) { + $bookmark = self::$bookmarks->findById( $id ); + if ( $bookmark == false ) { + Session::flash( 'error', 'Bookmark not found.' ); + return Redirect::to( 'bookmarks/index' ); + } + if ( $bookmark->createdBy != App::$activeUser->ID ) { + Session::flash( 'error', 'You do not have permission to modify this bookmark.' ); + return Redirect::to( 'bookmarks/index' ); + } + self::$bookmarks->unarchive( $id ); + Session::flash( 'success', 'Bookmark un-archived.' ); + return Redirect::to( 'bookmarks/index' ); + } + + public function refreshBookmark( $id = null ) { + $bookmark = self::$bookmarks->findById( $id ); + if ( $bookmark == false ) { + Session::flash( 'error', 'Bookmark not found.' ); + return Redirect::to( 'bookmarks/index' ); + } + if ( $bookmark->createdBy != App::$activeUser->ID ) { + Session::flash( 'error', 'You do not have permission to modify this bookmark.' ); + return Redirect::to( 'bookmarks/index' ); + } + $info = self::$bookmarks->refreshInfo( $id ); + if ( false == $info ) { + Session::flash( 'error', 'Issue refreshing your bookmark.' ); + return Redirect::to( 'bookmarks/bookmark/' . $bookmark->ID ); + } + Session::flash( 'success', 'Bookmark data refreshed.' ); + return Redirect::to( 'bookmarks/bookmark/' . $bookmark->ID ); + } + + public function import() { + self::$title = 'Bookmark Import - {SITENAME}'; + if ( !App::$isMember ) { + Issues::add( 'notice', 'You will need an active subscription to start importing bookmarks. You can check our Pricing page for more details.' ); + return Views::view( 'bookmarks.importExplainer' ); + } + + if ( ! Input::exists('submit') ) { + return Views::view( 'bookmarks.import' ); + } + + if ( ! Forms::check( 'importBookmarks' ) ) { + Issues::add( 'error', [ 'There was an error importing your bookmarks.' => Check::userErrors() ] ); + return Views::view( 'bookmarks.import' ); + } + + if ( isset( $_FILES['bookmark_file'] ) ) { + $file = $_FILES['bookmark_file']; + + if ($file['size'] > 1024 * 1024) { // 1024 KB = 1 MB + Issues::add( 'error', 'There is a 1 meg limit on bookmark imports at this time.' ); + return Views::view( 'bookmarks.import' ); + } + + $fileExtension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); + if ($fileExtension !== 'html') { + Issues::add( 'error', 'Invalid file type. Only .html files are allowed.' ); + return Views::view( 'bookmarks.import' ); + } + + $fileContent = file_get_contents($file['tmp_name']); + } else { + Issues::add( 'error', 'No Import detected' ); + return Views::view( 'bookmarks.import' ); + } + + $out = $this->parseBookmarks($fileContent); + $date = date('F j, Y'); + $description = 'Imported on ' . $date . ' from file: ' . $file['name']; + + $importFolder = self::$folders->create( 'New Import', 0, $description ); + foreach ($out as $folder => $bookmarks) { + $currentFolder = self::$folders->create( $folder, $importFolder, $description ); + foreach ($bookmarks as $index => $bookmark) { + self::$bookmarks->create( $bookmark['name'], $bookmark['url'], $currentFolder); + } + } + + Session::flash( 'success', 'Your Bookmark has been created.' ); + Redirect::to( 'bookmarks/bookmarks/'. $importFolder ); + } + + public function export() { + self::$title = 'Bookmark Export - {SITENAME}'; + if ( !App::$isMember ) { + Issues::add( 'notice', 'You will need an active subscription to start exporting bookmarks. You can check our Pricing page for more details.' ); + return Views::view( 'bookmarks.exportExplainer' ); + } + + $folders = self::$folders->byUser(); + + if ( ! Input::exists('submit') ) { + return Views::view( 'bookmarks.export', $folders ); + } + + if ( ! Forms::check( 'exportBookmarks' ) ) { + Issues::add( 'error', [ 'There was an error exporting your bookmarks.' => Check::userErrors() ] ); + return Views::view( 'bookmarks.export', $folders ); + } + + $htmlDoc = ''; + $htmlDoc .= '' . PHP_EOL; + $htmlDoc .= '' . PHP_EOL; + $htmlDoc .= '' . PHP_EOL; + $htmlDoc .= 'Bookmarks' . PHP_EOL; + $htmlDoc .= '

    Bookmarks

    ' . PHP_EOL; + $htmlDoc .= '

    ' . PHP_EOL; + foreach ( Input::post('BF_') as $key => $id ) { + if ( $id == 'unsorted' ) { + continue; + } + + $folder = self::$folders->findById( $id ); + if ( $folder == false ) { + Session::flash( 'error', 'Folder not found.' ); + return Redirect::to( 'bookmarks/index' ); + } + + $hiddenExcluded = ! ( Input::post('hiddenIncluded') ?? false ); + $archivedExcluded = ! ( Input::post('archivedIncluded') ?? false ); + + $links = self::$bookmarks->byFolder( $folder->ID, $hiddenExcluded, $archivedExcluded ); + $htmlDoc .= $this->exportFolder( $folder->title, $folder->createdAt, $folder->createdAt, $links ); + } + $htmlDoc .= '

    ' . PHP_EOL; + + $folder = UPLOAD_DIRECTORY . App::$activeUser->username; + if ( !file_exists( $folder ) ) { + mkdir( $folder, 0777, true ); + } + $file = $folder . DIRECTORY_SEPARATOR . 'export.html'; + + $result = file_put_contents( $file, $htmlDoc ); + + if ($result !== false) { + $filename = basename($file); + $downloadUrl = '/uploads/' . App::$activeUser->username . '/export.html'; + + Session::flash( 'success', 'Your Export is available for download here.' ); + Redirect::to( 'bookmarks/export' ); + } else { + Session::flash( 'error', 'There was an issue exporting your bookmarks, please try again.' ); + return Redirect::to( 'bookmarks/export' ); + } + } + + public function share() { + $panelArray = []; + $folders = self::$folders->byUser(); + if ( empty( $folders ) ) { + return Views::view( 'bookmarks.shareExplainer' ); + } + foreach ( $folders as $key => $folder ) { + $panel = new \stdClass(); + $folderObject = new \stdClass(); + if ( $folder->privacy == 'private' ) { + $links = self::$bookmarks->publicByFolder( $folder->ID ); + } else { + $links = self::$bookmarks->byFolder( $folder->ID ); + } + $folderObject->bookmarks = $links; + + $folderObject->ID = $folder->ID; + $folderObject->uuid = $folder->uuid; + $folderObject->title = $folder->title; + $folderObject->color = $folder->color; + $folderObject->privacyBadge = $folder->privacyBadge; + + $folderObject->bookmarkListRows = Views::simpleView( 'bookmarks.components.shareListRows', $folderObject->bookmarks ); + $panel->panel = Views::simpleView( 'bookmarks.components.shareListPanel', [$folderObject] ); + $panelArray[] = $panel; + } + return Views::view( 'bookmarks.share', $panelArray ); + } + + + /** + * Private + */ + private function exportFolder( $title, $editedAt, $createdAt, $links ) { + $htmlDoc = '

    '.$title.'

    ' . PHP_EOL; + $htmlDoc .= '

    ' . PHP_EOL; + if ( ! empty( $links ) ) { + foreach ( $links as $key => $link ) { + $htmlDoc .= $this->exportLink( $link->url, $link->icon, $link->createdAt, $link->title ); + } + } + $htmlDoc .= '

    ' . PHP_EOL; + return $htmlDoc; + } + + private function exportLink( $url, $icon, $createdAt, $title ) { + $htmlDoc = '

    '.$title.'' . PHP_EOL; + return $htmlDoc; + } + + private function parseBookmarks($htmlContent) { + $started = false; + $out = []; + $currentFolder = []; + $folderName = ['unknown']; + $lines = explode("\n", $htmlContent); + foreach ($lines as $line) { + if ( $started == false ) { + if (preg_match("/

    (.*?)<\/h1>/i", $line, $matches)) { + $started = true; + } + continue; + } + + if (preg_match('/

    /i', $line, $matches)) { + continue; + } + + if (preg_match('/(.*?)<\/h3>/i', $line, $matches)) { + $newFolder = $matches[2]; + $out[$newFolder] = []; + array_unshift($folderName, $newFolder ); + continue; + } + + if (preg_match('/<\/DL>

    /i', $line, $matches)) { + array_shift($folderName); + continue; + } + + if (preg_match('/(.*?)<\/A>/i', $line, $matches)) { + $href = $matches[1]; + $guts = $matches[2]; + $text = $matches[3]; + $added = ''; + $icon = ''; + + if (preg_match('/ADD_DATE="(.*?)"/i', $guts, $addMatches)) { + $added = $addMatches[1]; + } + if (preg_match('/ICON="(.*?)"/i', $guts, $iconMatches)) { + $icon = $iconMatches[1]; + } + + $currentFolder = $folderName[0]; + $out[$currentFolder][] = [ + 'name' => $text, + 'url' => $href, + 'addDate' => $added, + 'icon' => $icon, + 'folderName' => $folderName[0], + ]; + continue; + } + } + return $out; + } + + private function setFolderSelect( $folderID ) { + $options = self::$folders->simpleByUser(); + $out = ''; + $out .= '

    '; + $folderSelect = Template::parse( $out ); + Components::set( 'folderSelect', $folderSelect ); + } + + private function setPrefToggles() { + $prefsArray = [ + 'editModeSwitch', + 'showArchivedSwitch', + 'showHiddenSwitch', + 'archiveButtonSwitch', + 'visibilityButtonSwitch', + 'privacyButtonSwitch', + 'shareButtonSwitch', + 'addButtonSwitch' + ]; + foreach ($prefsArray as $key => $name) { + if ( empty( App::$activeUser->prefs[$name] ) ) { + Components::set( $name . '_IS_CHECKED', '' ); + } else { + Components::set( $name . '_IS_CHECKED', ' checked' ); + } + } + } + + private function setDashToggles( $current = [] ) { + $prefsArray = [ + 'editModeSwitch', + 'showArchivedSwitch', + 'showHiddenSwitch', + 'archiveButtonSwitch', + 'visibilityButtonSwitch', + 'privacyButtonSwitch', + 'shareButtonSwitch', + 'addButtonSwitch' + ]; + foreach ( $prefsArray as $key => $name ) { + Debug::error( $name ); + Debug::error( $current ); + if ( ! in_array( $name, $current ) ) { + Components::set( $name . '_IS_CHECKED', '' ); + } else { + Debug::error( '_IS_CHECKED' ); + Components::set( $name . '_IS_CHECKED', ' checked' ); + } + } + } +} diff --git a/app/plugins/bookmarks/controllers/extensions.php b/app/plugins/bookmarks/controllers/extensions.php new file mode 100644 index 0000000..53b47af --- /dev/null +++ b/app/plugins/bookmarks/controllers/extensions.php @@ -0,0 +1,148 @@ + + * @link https://TheTempusProject.com + * @license https://opensource.org/licenses/MIT [MIT LICENSE] + */ +namespace TheTempusProject\Controllers; + +use TheTempusProject\Classes\Controller; +use TheTempusProject\Houdini\Classes\Views; +use TheTempusProject\Houdini\Classes\Issues; +use TheTempusProject\TheTempusProject as App; +use TheTempusProject\Hermes\Functions\Route as Routes; +use TheTempusProject\Models\Token; +use TheTempusProject\Houdini\Classes\Components; +use TheTempusProject\Models\Folders; +use TheTempusProject\Houdini\Classes\Template; +use TheTempusProject\Bedrock\Functions\Input; + +class Extensions extends Controller { + public function index() { + self::$title = 'Browser Extensions'; + self::$pageDescription = 'Our extensions cover all major browsers with the notable exception of Safari. Chrome, Opera, Brave, Firefox, and Edge are all represented; giving users a simple way to add bookmarks and folders from any page.'; + if ( App::$isLoggedIn ) { + Issues::add( 'success', 'We also have a simple solution to using the app from your mobile devices here.' ); + } + Views::view( 'bookmarks.extensions.index' ); + } + + public function mobile() { + self::$title = 'Mobile Bookmarklet'; + self::$pageDescription = 'When you find yourself on the go, sometimes an extension isn\'t an option. For those times, we have the mobile bookmarklet to allow you to add bookmarks from your mobile devices.'; + + if ( ! App::$isLoggedIn ) { + Issues::add( 'notice', 'Since the bookmarklet is tied directly to your account, you will need to log in to generate the code for your account.' ); + return Views::view( 'bookmarks.extensions.bookmarkletExplainer' ); + } + + if ( Input::exists('privacy') ) { + $privacy = 'const privacy = "' . Input::post('privacy') . '";'; + } else { + $privacy = 'const privacy = prompt("Enter privacy level (e.g., public/private):");'; + } + Components::set( 'BK_JS_PRIVACY', $privacy ); + + if ( Input::exists('includeDescription') ) { + $description = 'const notes = prompt("Enter a description (optional):");'; + } else { + $description = ''; + } + Components::set( 'BK_JS_NOTES', $description ); + + if ( Input::exists('includeColor') ) { + $color = 'const color = "'.Input::post('color').'";'; + } else { + $color = ''; + } + Components::set( 'BK_JS_COLOR', $color ); + + if ( Input::exists('includeFolder') ) { + $folder = 'const folder = "'.Input::post('folder_id').'";'; + } else { + $folder = ''; + } + Components::set( 'BK_JS_FOLDER', $folder ); + + $this->setFolderSelect(Input::post('folder')); + + $tokens = new Token; + $apiKey = $tokens->findOrCreateUserToken( App::$activeUser->ID, true ); + $apiUrl = Routes::getAddress() . 'api/bookmarks/create'; + Components::set( 'BK_API_KEY', $apiKey ); + Components::set( 'BK_API_URL', $apiUrl ); + + Views::view( 'bookmarks.extensions.bookmarklet' ); + } + + public function chrome() { + self::$title = 'Chrome Extension'; + self::$pageDescription = 'Our Chrome extension allows you to quickly add new bookmarks or folders right from your browser\'s toolbar.'; + Views::view( 'bookmarks.extensions.chrome' ); + } + + public function firefox() { + self::$title = 'Firefox Extension'; + self::$pageDescription = 'Our Firefox extension allows you to quickly add new bookmarks or folders right from your browser\'s toolbar.'; + Views::view( 'bookmarks.extensions.firefox' ); + } + + public function opera() { + self::$title = 'Opera Extension'; + self::$pageDescription = 'Our Opera extension allows you to quickly add new bookmarks or folders right from your browser\'s toolbar.'; + Views::view( 'bookmarks.extensions.opera' ); + } + + public function edge() { + self::$title = 'Edge Extension'; + self::$pageDescription = 'Our Edge extension allows you to quickly add new bookmarks or folders right from your browser\'s toolbar.'; + Views::view( 'bookmarks.extensions.edge' ); + } + + public function brave() { + self::$title = 'Brave Extension'; + self::$pageDescription = 'Our Brave extension allows you to quickly add new bookmarks or folders right from your browser\'s toolbar.'; + Views::view( 'bookmarks.extensions.brave' ); + } + + public function safari() { + self::$title = 'Safari Extension'; + self::$pageDescription = 'Our Safari extension allows you to quickly add new bookmarks or folders right from your browser\'s toolbar.'; + Views::view( 'bookmarks.extensions.safari' ); + } + + private function setFolderSelect( $folderID = null ) { + $folders = new Folders; + $options = $folders->simpleByUser(); + $out = ''; + $out .= ''; + $folderSelect = Template::parse( $out ); + Components::set( 'folderSelect', $folderSelect ); + } +} diff --git a/app/plugins/bookmarks/controllers/shared.php b/app/plugins/bookmarks/controllers/shared.php new file mode 100644 index 0000000..322c709 --- /dev/null +++ b/app/plugins/bookmarks/controllers/shared.php @@ -0,0 +1,107 @@ + + * @link https://TheTempusProject.com + * @license https://opensource.org/licenses/MIT [MIT LICENSE] + */ +namespace TheTempusProject\Controllers; + +use TheTempusProject\Hermes\Functions\Redirect; +use TheTempusProject\Bedrock\Functions\Session; +use TheTempusProject\Houdini\Classes\Views; +use TheTempusProject\Classes\Controller; +use TheTempusProject\Models\Bookmarks as Bookmark; +use TheTempusProject\Models\Folders; +use TheTempusProject\TheTempusProject as App; +use TheTempusProject\Houdini\Classes\Components; +use TheTempusProject\Hermes\Functions\Route as Routes; + +class Shared extends Controller { + protected static $bookmarks; + protected static $folders; + + public function __construct() { + parent::__construct(); + self::$bookmarks = new Bookmark; + self::$folders = new Folders; + self::$title = 'Bookmarks - {SITENAME}'; + self::$pageDescription = 'Add and save url bookmarks here.'; + Components::set( 'SITE_URL', Routes::getAddress() ); + } + + public function shared( $type = '', $id = '' ) { + if ( empty( $type ) ) { + Session::flash( 'error', 'Unknown share' ); + return Redirect::to( 'home/index' ); + } + + if ( empty( $id ) ) { + Session::flash( 'error', 'Unknown share' ); + return Redirect::to( 'home/index' ); + } + + $type = strtolower( $type ); + if ( ! in_array( $type, ['link','folder'] ) ) { + Session::flash( 'error', 'Unknown share' ); + return Redirect::to( 'home/index' ); + } + + $this->$type( $id ); + } + + public function link( $id = '' ) { + if ( empty( $id ) ) { + Session::flash( 'error', 'Unknown share' ); + return Redirect::to( 'home/index' ); + } + $bookmark = self::$bookmarks->findByUuid( $id ); + if ( $bookmark == false ) { + Session::flash( 'error', 'Unknown share' ); + return Redirect::to( 'home/index' ); + } + if ( $bookmark->createdBy != App::$activeUser->ID ) { + if ( $bookmark->privacy == 'private' ) { + if ( empty( $bookmark->folderID ) ) { + Session::flash( 'error', 'Unknown share' ); + return Redirect::to( 'home/index' ); + } + $folder = self::$folders->findByUuid( $bookmark->folderID ); + if ( $folder == false ) { + Session::flash( 'error', 'Unknown share' ); + return Redirect::to( 'home/index' ); + } + if ( $folder->privacy == 'private' ) { + Session::flash( 'error', 'Unknown share' ); + return Redirect::to( 'home/index' ); + } + } + } + return Views::view( 'bookmarks.public.bookmark', $bookmark ); + } + + public function folder( $id = '' ) { + if ( empty( $id ) ) { + Session::flash( 'error', 'Unknown share' ); + return Redirect::to( 'home/index' ); + } + $folder = self::$folders->findByUuid( $id ); + if ( $folder == false ) { + Session::flash( 'error', 'Unknown share' ); + return Redirect::to( 'home/index' ); + } + if ( $folder->privacy == 'private' ) { + Session::flash( 'error', 'Unknown share' ); + return Redirect::to( 'home/index' ); + } + $folder->bookmarks = self::$bookmarks->unsafeByFolder( $folder->ID ); + $folder->bookmarkListRows = Views::simpleView( 'bookmarks.components.publicListRows', $folder->bookmarks ); + $folder->panel = Views::simpleView( 'bookmarks.components.publicList', [$folder] ); + return Views::view( 'bookmarks.public.folder', $folder ); + } +} diff --git a/app/plugins/bookmarks/controllers/tutorials.php b/app/plugins/bookmarks/controllers/tutorials.php new file mode 100644 index 0000000..a058562 --- /dev/null +++ b/app/plugins/bookmarks/controllers/tutorials.php @@ -0,0 +1,114 @@ + + * @link https://TheTempusProject.com + * @license https://opensource.org/licenses/MIT [MIT LICENSE] + */ +namespace TheTempusProject\Controllers; + +use TheTempusProject\Houdini\Classes\Issues; +use TheTempusProject\Houdini\Classes\Views; +use TheTempusProject\Classes\Controller; +use TheTempusProject\Houdini\Classes\Navigation; +use TheTempusProject\Houdini\Classes\Components; +use TheTempusProject\Bedrock\Functions\Input; + +class Tutorials extends Controller { + public function __construct() { + parent::__construct(); + self::$title = 'Tutorials - {SITENAME}'; + self::$pageDescription = 'We have detailed walkthroughs on how to perform a number of tasks for every major browser.'; + } + + public function index( $browser = '', $tutorial = '' ) { + return Views::view( 'bookmarks.tutorials.list' ); + } + + public function brave( $tutorial = '' ) { + Navigation::setCrumbComponent( 'tutorialCrumbs', Input::get( 'url' ) ); + + if ( ! in_array( $tutorial, ['pin','settings','import','export'] ) ) { + $test = new \stdClass(); + $test->pretty = 'Brave'; + $test->printed = 'brave'; + return Views::view( 'bookmarks.tutorials.card', [ $test ] ); + } + return Views::view( 'bookmarks.tutorials.brave.' . $tutorial ); + } + + public function chrome( $tutorial = '' ) { + Navigation::setCrumbComponent( 'tutorialCrumbs', Input::get( 'url' ) ); + + if ( ! in_array( $tutorial, ['pin','settings','import','export'] ) ) { + $test = new \stdClass(); + $test->pretty = 'Chrome'; + $test->printed = 'chrome'; + return Views::view( 'bookmarks.tutorials.card', [ $test ] ); + } + + return Views::view( 'bookmarks.tutorials.chrome.' . $tutorial ); + } + + public function edge( $tutorial = '' ) { + Navigation::setCrumbComponent( 'tutorialCrumbs', Input::get( 'url' ) ); + + if ( ! in_array( $tutorial, ['pin','settings','import','export'] ) ) { + $test = new \stdClass(); + $test->pretty = 'Edge'; + $test->printed = 'edge'; + return Views::view( 'bookmarks.tutorials.card', [ $test ] ); + } + return Views::view( 'bookmarks.tutorials.edge.' . $tutorial ); + } + + public function opera( $tutorial = '' ) { + Navigation::setCrumbComponent( 'tutorialCrumbs', Input::get( 'url' ) ); + + if ( ! in_array( $tutorial, ['pin','settings','import','export'] ) ) { + $test = new \stdClass(); + $test->pretty = 'Opera'; + $test->printed = 'opera'; + return Views::view( 'bookmarks.tutorials.card', [ $test ] ); + } + return Views::view( 'bookmarks.tutorials.opera.' . $tutorial ); + } + + public function firefox( $tutorial = '' ) { + Navigation::setCrumbComponent( 'tutorialCrumbs', Input::get( 'url' ) ); + + if ( ! in_array( $tutorial, ['pin','settings','import','export'] ) ) { + $test = new \stdClass(); + $test->pretty = 'Firefox'; + $test->printed = 'firefox'; + return Views::view( 'bookmarks.tutorials.card', [ $test ] ); + } + return Views::view( 'bookmarks.tutorials.firefox.' . $tutorial ); + } + + public function safari( $tutorial = '' ) { + Issues::add( 'notice', 'Safari is not supported at this time.' ); + return; + if ( ! in_array( $tutorial, ['pin','settings','import','export'] ) ) { + Issues::add( 'notice', 'Unknown tutorial' ); + return Views::view( 'bookmarks.tutorials.list' ); + } + return Views::view( 'bookmarks.tutorials.safari.' . $tutorial ); + } + + public function mobile( $tutorial = '' ) { + Navigation::setCrumbComponent( 'tutorialCrumbs', Input::get( 'url' ) ); + if ( ! in_array( $tutorial, ['iphone','android'] ) ) { + $test = new \stdClass(); + $test->pretty = 'Mobile'; + $test->printed = 'mobile'; + return Views::view( 'bookmarks.tutorials.mobileCard', [ $test ] ); + } + return Views::view( 'bookmarks.tutorials.mobile.' . $tutorial ); + } +} \ No newline at end of file diff --git a/app/plugins/bookmarks/forms.php b/app/plugins/bookmarks/forms.php new file mode 100644 index 0000000..a76a903 --- /dev/null +++ b/app/plugins/bookmarks/forms.php @@ -0,0 +1,225 @@ + + * @link https://TheTempusProject.com + * @license https://opensource.org/licenses/MIT [MIT LICENSE] + */ +namespace TheTempusProject\Plugins\Bookmarks; + +use TheTempusProject\Bedrock\Functions\Input; +use TheTempusProject\Bedrock\Functions\Check; +use TheTempusProject\Classes\Forms; + +class BookmarksForms extends Forms { + /** + * Adds these functions to the form list. + */ + public function __construct() { + self::addHandler( 'createBookmark', __CLASS__, 'createBookmark' ); + self::addHandler( 'createFolder', __CLASS__, 'createFolder' ); + self::addHandler( 'editBookmark', __CLASS__, 'editBookmark' ); + self::addHandler( 'editFolder', __CLASS__, 'editFolder' ); + self::addHandler( 'importBookmarks', __CLASS__, 'importBookmarks' ); + self::addHandler( 'exportBookmarks', __CLASS__, 'exportBookmarks' ); + self::addHandler( 'createDashboard', __CLASS__, 'createDashboard' ); + self::addHandler( 'editDashboard', __CLASS__, 'editDashboard' ); + self::addHandler( 'updateDashboard', __CLASS__, 'updateDashboard' ); + } + + public static function createBookmark() { + // if ( ! Input::exists( 'title' ) ) { + // Check::addUserError( 'You must include a title.' ); + // return false; + // } + if ( ! Input::exists( 'url' ) ) { + Check::addUserError( 'You must include a url.' ); + return false; + } + // if ( ! Input::exists( 'color' ) ) { + // Check::addUserError( 'You must include a color.' ); + // return false; + // } + // if ( ! Input::exists( 'privacy' ) ) { + // Check::addUserError( 'You must include a privacy.' ); + // return false; + // } + // if ( !self::token() ) { + // Check::addUserError( 'token - comment out later.' ); + // return false; + // } + return true; + } + + public static function createFolder() { + if ( ! Input::exists( 'title' ) ) { + Check::addUserError( 'You must include a title.' ); + return false; + } + // if ( ! Input::exists( 'color' ) ) { + // Check::addUserError( 'You must include a color.' ); + // return false; + // } + // if ( ! Input::exists( 'privacy' ) ) { + // Check::addUserError( 'You must include a privacy.' ); + // return false; + // } + // if ( ! self::token() ) { + // Check::addUserError( 'token - comment out later.' ); + // return false; + // } + return true; + } + + public static function editBookmark() { + // if ( ! Input::exists( 'title' ) ) { + // Check::addUserError( 'You must include a title.' ); + // return false; + // } + if ( ! Input::exists( 'url' ) ) { + Check::addUserError( 'You must include a url.' ); + return false; + } + // if ( ! Input::exists( 'color' ) ) { + // Check::addUserError( 'You must include a color.' ); + // return false; + // } + // if ( ! Input::exists( 'privacy' ) ) { + // Check::addUserError( 'You must include a privacy.' ); + // return false; + // } + // if ( !self::token() ) { + // Check::addUserError( 'token - comment out later.' ); + // return false; + // } + return true; + } + + public static function editFolder() { + if ( ! Input::exists( 'submit' ) ) { + return false; + } + if ( ! Input::exists( 'title' ) ) { + Check::addUserError( 'You must include a title.' ); + return false; + } + // if ( ! Input::exists( 'color' ) ) { + // Check::addUserError( 'You must include a color.' ); + // return false; + // } + // if ( ! Input::exists( 'privacy' ) ) { + // Check::addUserError( 'You must include a privacy.' ); + // return false; + // } + // if ( !self::token() ) { + // Check::addUserError( 'token - comment out later.' ); + // return false; + // } + return true; + } + + public static function importBookmarks() { + if ( ! Input::exists( 'submit' ) ) { + return false; + } + // if ( ! Input::exists( 'title' ) ) { + // Check::addUserError( 'You must include a title.' ); + // return false; + // } + // if ( ! Input::exists( 'color' ) ) { + // Check::addUserError( 'You must include a color.' ); + // return false; + // } + // if ( ! Input::exists( 'privacy' ) ) { + // Check::addUserError( 'You must include a privacy.' ); + // return false; + // } + // if ( !self::token() ) { + // Check::addUserError( 'token - comment out later.' ); + // return false; + // } + return true; + } + + public static function exportBookmarks() { + if ( ! Input::exists( 'submit' ) ) { + return false; + } + if ( ! Input::exists( 'BF_' ) ) { + return false; + } + // if ( ! Input::exists( 'title' ) ) { + // Check::addUserError( 'You must include a title.' ); + // return false; + // } + // if ( ! Input::exists( 'color' ) ) { + // Check::addUserError( 'You must include a color.' ); + // return false; + // } + // if ( ! Input::exists( 'privacy' ) ) { + // Check::addUserError( 'You must include a privacy.' ); + // return false; + // } + // if ( !self::token() ) { + // Check::addUserError( 'token - comment out later.' ); + // return false; + // } + return true; + } + + public static function createDashboard() { + if ( ! Input::exists( 'submit' ) ) { + return false; + } + if ( ! Input::exists( 'title' ) ) { + Check::addUserError( 'You must include a title.' ); + return false; + } + if ( ! Input::exists( 'link_order' ) ) { + Check::addUserError( 'You must include at least 1 link or folder.' ); + return false; + } + if ( !self::token() ) { + Check::addUserError( 'There was an issue with your request.' ); + return false; + } + return true; + } + + public static function updateDashboard() { + if ( ! Input::exists( 'submit' ) ) { + return false; + } + if ( !self::token() ) { + Check::addUserError( 'There was an issue with your request.' ); + return false; + } + return true; + } + + public static function editDashboard() { + if ( ! Input::exists( 'submit' ) ) { + return false; + } + if ( ! Input::exists( 'title' ) ) { + Check::addUserError( 'You must include a title.' ); + return false; + } + if ( ! Input::exists( 'link_order' ) ) { + Check::addUserError( 'You must include at least 1 link or folder.' ); + return false; + } + if ( !self::token() ) { + Check::addUserError( 'There was an issue with your request.' ); + return false; + } + return true; + } +} + +new BookmarksForms; \ No newline at end of file diff --git a/app/plugins/bookmarks/images/android/add.png b/app/plugins/bookmarks/images/android/add.png new file mode 100644 index 0000000..df8c241 Binary files /dev/null and b/app/plugins/bookmarks/images/android/add.png differ diff --git a/app/plugins/bookmarks/images/android/add2.png b/app/plugins/bookmarks/images/android/add2.png new file mode 100644 index 0000000..649bdb8 Binary files /dev/null and b/app/plugins/bookmarks/images/android/add2.png differ diff --git a/app/plugins/bookmarks/images/android/add3.png b/app/plugins/bookmarks/images/android/add3.png new file mode 100644 index 0000000..0a9eecd Binary files /dev/null and b/app/plugins/bookmarks/images/android/add3.png differ diff --git a/app/plugins/bookmarks/images/android/added.png b/app/plugins/bookmarks/images/android/added.png new file mode 100644 index 0000000..f81c901 Binary files /dev/null and b/app/plugins/bookmarks/images/android/added.png differ diff --git a/app/plugins/bookmarks/images/brave/bookmark-manager.png b/app/plugins/bookmarks/images/brave/bookmark-manager.png new file mode 100644 index 0000000..3024811 Binary files /dev/null and b/app/plugins/bookmarks/images/brave/bookmark-manager.png differ diff --git a/app/plugins/bookmarks/images/brave/bookmark-menu.png b/app/plugins/bookmarks/images/brave/bookmark-menu.png new file mode 100644 index 0000000..b6d3309 Binary files /dev/null and b/app/plugins/bookmarks/images/brave/bookmark-menu.png differ diff --git a/app/plugins/bookmarks/images/brave/bookmarks.png b/app/plugins/bookmarks/images/brave/bookmarks.png new file mode 100644 index 0000000..b431cbe Binary files /dev/null and b/app/plugins/bookmarks/images/brave/bookmarks.png differ diff --git a/app/plugins/bookmarks/images/brave/export.png b/app/plugins/bookmarks/images/brave/export.png new file mode 100644 index 0000000..87dd1c6 Binary files /dev/null and b/app/plugins/bookmarks/images/brave/export.png differ diff --git a/app/plugins/bookmarks/images/brave/extensions-icon.png b/app/plugins/bookmarks/images/brave/extensions-icon.png new file mode 100644 index 0000000..bcc2cc1 Binary files /dev/null and b/app/plugins/bookmarks/images/brave/extensions-icon.png differ diff --git a/app/plugins/bookmarks/images/brave/import.png b/app/plugins/bookmarks/images/brave/import.png new file mode 100644 index 0000000..622053c Binary files /dev/null and b/app/plugins/bookmarks/images/brave/import.png differ diff --git a/app/plugins/bookmarks/images/brave/options.png b/app/plugins/bookmarks/images/brave/options.png new file mode 100644 index 0000000..10b9339 Binary files /dev/null and b/app/plugins/bookmarks/images/brave/options.png differ diff --git a/app/plugins/bookmarks/images/brave/pin.png b/app/plugins/bookmarks/images/brave/pin.png new file mode 100644 index 0000000..8e800f0 Binary files /dev/null and b/app/plugins/bookmarks/images/brave/pin.png differ diff --git a/app/plugins/bookmarks/images/brave/pinned.png b/app/plugins/bookmarks/images/brave/pinned.png new file mode 100644 index 0000000..f94fb59 Binary files /dev/null and b/app/plugins/bookmarks/images/brave/pinned.png differ diff --git a/app/plugins/bookmarks/images/brave/settings-page.png b/app/plugins/bookmarks/images/brave/settings-page.png new file mode 100644 index 0000000..f1671dc Binary files /dev/null and b/app/plugins/bookmarks/images/brave/settings-page.png differ diff --git a/app/plugins/bookmarks/images/brave/settings.png b/app/plugins/bookmarks/images/brave/settings.png new file mode 100644 index 0000000..668b048 Binary files /dev/null and b/app/plugins/bookmarks/images/brave/settings.png differ diff --git a/app/plugins/bookmarks/images/chrome/bookmark-manager.png b/app/plugins/bookmarks/images/chrome/bookmark-manager.png new file mode 100644 index 0000000..1dd091a Binary files /dev/null and b/app/plugins/bookmarks/images/chrome/bookmark-manager.png differ diff --git a/app/plugins/bookmarks/images/chrome/bookmark-menu.png b/app/plugins/bookmarks/images/chrome/bookmark-menu.png new file mode 100644 index 0000000..f559010 Binary files /dev/null and b/app/plugins/bookmarks/images/chrome/bookmark-menu.png differ diff --git a/app/plugins/bookmarks/images/chrome/bookmarks.png b/app/plugins/bookmarks/images/chrome/bookmarks.png new file mode 100644 index 0000000..def6eb6 Binary files /dev/null and b/app/plugins/bookmarks/images/chrome/bookmarks.png differ diff --git a/app/plugins/bookmarks/images/chrome/chrome-add.jpg b/app/plugins/bookmarks/images/chrome/chrome-add.jpg new file mode 100644 index 0000000..579d5b8 Binary files /dev/null and b/app/plugins/bookmarks/images/chrome/chrome-add.jpg differ diff --git a/app/plugins/bookmarks/images/chrome/chrome-login.jpg b/app/plugins/bookmarks/images/chrome/chrome-login.jpg new file mode 100644 index 0000000..db42731 Binary files /dev/null and b/app/plugins/bookmarks/images/chrome/chrome-login.jpg differ diff --git a/app/plugins/bookmarks/images/chrome/chrome-marquee-promo-tile.png b/app/plugins/bookmarks/images/chrome/chrome-marquee-promo-tile.png new file mode 100644 index 0000000..3c9da7f Binary files /dev/null and b/app/plugins/bookmarks/images/chrome/chrome-marquee-promo-tile.png differ diff --git a/app/plugins/bookmarks/images/chrome/chrome-settings.jpg b/app/plugins/bookmarks/images/chrome/chrome-settings.jpg new file mode 100644 index 0000000..97949b7 Binary files /dev/null and b/app/plugins/bookmarks/images/chrome/chrome-settings.jpg differ diff --git a/app/plugins/bookmarks/images/chrome/chrome-small-promo-tile.png b/app/plugins/bookmarks/images/chrome/chrome-small-promo-tile.png new file mode 100644 index 0000000..015a2f2 Binary files /dev/null and b/app/plugins/bookmarks/images/chrome/chrome-small-promo-tile.png differ diff --git a/app/plugins/bookmarks/images/chrome/export.png b/app/plugins/bookmarks/images/chrome/export.png new file mode 100644 index 0000000..42d4fe1 Binary files /dev/null and b/app/plugins/bookmarks/images/chrome/export.png differ diff --git a/app/plugins/bookmarks/images/chrome/extensions-icon.png b/app/plugins/bookmarks/images/chrome/extensions-icon.png new file mode 100644 index 0000000..34dfd50 Binary files /dev/null and b/app/plugins/bookmarks/images/chrome/extensions-icon.png differ diff --git a/app/plugins/bookmarks/images/chrome/import.png b/app/plugins/bookmarks/images/chrome/import.png new file mode 100644 index 0000000..9d6cb93 Binary files /dev/null and b/app/plugins/bookmarks/images/chrome/import.png differ diff --git a/app/plugins/bookmarks/images/chrome/options.png b/app/plugins/bookmarks/images/chrome/options.png new file mode 100644 index 0000000..9fa20a6 Binary files /dev/null and b/app/plugins/bookmarks/images/chrome/options.png differ diff --git a/app/plugins/bookmarks/images/chrome/pin.png b/app/plugins/bookmarks/images/chrome/pin.png new file mode 100644 index 0000000..eb90300 Binary files /dev/null and b/app/plugins/bookmarks/images/chrome/pin.png differ diff --git a/app/plugins/bookmarks/images/chrome/pinned.png b/app/plugins/bookmarks/images/chrome/pinned.png new file mode 100644 index 0000000..7f21ba0 Binary files /dev/null and b/app/plugins/bookmarks/images/chrome/pinned.png differ diff --git a/app/plugins/bookmarks/images/chrome/settings-page.png b/app/plugins/bookmarks/images/chrome/settings-page.png new file mode 100644 index 0000000..e662028 Binary files /dev/null and b/app/plugins/bookmarks/images/chrome/settings-page.png differ diff --git a/app/plugins/bookmarks/images/chrome/settings.png b/app/plugins/bookmarks/images/chrome/settings.png new file mode 100644 index 0000000..ba7f40b Binary files /dev/null and b/app/plugins/bookmarks/images/chrome/settings.png differ diff --git a/app/plugins/bookmarks/images/edge/bookmark-options.png b/app/plugins/bookmarks/images/edge/bookmark-options.png new file mode 100644 index 0000000..f6b0743 Binary files /dev/null and b/app/plugins/bookmarks/images/edge/bookmark-options.png differ diff --git a/app/plugins/bookmarks/images/edge/edge-add.png b/app/plugins/bookmarks/images/edge/edge-add.png new file mode 100644 index 0000000..547cf55 Binary files /dev/null and b/app/plugins/bookmarks/images/edge/edge-add.png differ diff --git a/app/plugins/bookmarks/images/edge/edge-login.png b/app/plugins/bookmarks/images/edge/edge-login.png new file mode 100644 index 0000000..7e27a1c Binary files /dev/null and b/app/plugins/bookmarks/images/edge/edge-login.png differ diff --git a/app/plugins/bookmarks/images/edge/edge-settings.png b/app/plugins/bookmarks/images/edge/edge-settings.png new file mode 100644 index 0000000..db02594 Binary files /dev/null and b/app/plugins/bookmarks/images/edge/edge-settings.png differ diff --git a/app/plugins/bookmarks/images/edge/edge-small-promo-tile.png b/app/plugins/bookmarks/images/edge/edge-small-promo-tile.png new file mode 100644 index 0000000..015a2f2 Binary files /dev/null and b/app/plugins/bookmarks/images/edge/edge-small-promo-tile.png differ diff --git a/app/plugins/bookmarks/images/edge/export.png b/app/plugins/bookmarks/images/edge/export.png new file mode 100644 index 0000000..69d71fb Binary files /dev/null and b/app/plugins/bookmarks/images/edge/export.png differ diff --git a/app/plugins/bookmarks/images/edge/extensions-icon.png b/app/plugins/bookmarks/images/edge/extensions-icon.png new file mode 100644 index 0000000..3d18c16 Binary files /dev/null and b/app/plugins/bookmarks/images/edge/extensions-icon.png differ diff --git a/app/plugins/bookmarks/images/edge/favorites.png b/app/plugins/bookmarks/images/edge/favorites.png new file mode 100644 index 0000000..f45bf3e Binary files /dev/null and b/app/plugins/bookmarks/images/edge/favorites.png differ diff --git a/app/plugins/bookmarks/images/edge/import-complete.png b/app/plugins/bookmarks/images/edge/import-complete.png new file mode 100644 index 0000000..843469a Binary files /dev/null and b/app/plugins/bookmarks/images/edge/import-complete.png differ diff --git a/app/plugins/bookmarks/images/edge/import-menu.png b/app/plugins/bookmarks/images/edge/import-menu.png new file mode 100644 index 0000000..7d86967 Binary files /dev/null and b/app/plugins/bookmarks/images/edge/import-menu.png differ diff --git a/app/plugins/bookmarks/images/edge/import-select.png b/app/plugins/bookmarks/images/edge/import-select.png new file mode 100644 index 0000000..ac3d844 Binary files /dev/null and b/app/plugins/bookmarks/images/edge/import-select.png differ diff --git a/app/plugins/bookmarks/images/edge/import-start.png b/app/plugins/bookmarks/images/edge/import-start.png new file mode 100644 index 0000000..8dfb0d1 Binary files /dev/null and b/app/plugins/bookmarks/images/edge/import-start.png differ diff --git a/app/plugins/bookmarks/images/edge/import.png b/app/plugins/bookmarks/images/edge/import.png new file mode 100644 index 0000000..c0b8a67 Binary files /dev/null and b/app/plugins/bookmarks/images/edge/import.png differ diff --git a/app/plugins/bookmarks/images/edge/options.png b/app/plugins/bookmarks/images/edge/options.png new file mode 100644 index 0000000..2654977 Binary files /dev/null and b/app/plugins/bookmarks/images/edge/options.png differ diff --git a/app/plugins/bookmarks/images/edge/pin.png b/app/plugins/bookmarks/images/edge/pin.png new file mode 100644 index 0000000..6890a40 Binary files /dev/null and b/app/plugins/bookmarks/images/edge/pin.png differ diff --git a/app/plugins/bookmarks/images/edge/pinned.png b/app/plugins/bookmarks/images/edge/pinned.png new file mode 100644 index 0000000..9b685c7 Binary files /dev/null and b/app/plugins/bookmarks/images/edge/pinned.png differ diff --git a/app/plugins/bookmarks/images/edge/settings-page.png b/app/plugins/bookmarks/images/edge/settings-page.png new file mode 100644 index 0000000..2b2d9ab Binary files /dev/null and b/app/plugins/bookmarks/images/edge/settings-page.png differ diff --git a/app/plugins/bookmarks/images/edge/settings.png b/app/plugins/bookmarks/images/edge/settings.png new file mode 100644 index 0000000..68ffede Binary files /dev/null and b/app/plugins/bookmarks/images/edge/settings.png differ diff --git a/app/plugins/bookmarks/images/firefox/added.png b/app/plugins/bookmarks/images/firefox/added.png new file mode 100644 index 0000000..0c4dc64 Binary files /dev/null and b/app/plugins/bookmarks/images/firefox/added.png differ diff --git a/app/plugins/bookmarks/images/firefox/bookmarks.png b/app/plugins/bookmarks/images/firefox/bookmarks.png new file mode 100644 index 0000000..33b7050 Binary files /dev/null and b/app/plugins/bookmarks/images/firefox/bookmarks.png differ diff --git a/app/plugins/bookmarks/images/firefox/enable.png b/app/plugins/bookmarks/images/firefox/enable.png new file mode 100644 index 0000000..fc2f578 Binary files /dev/null and b/app/plugins/bookmarks/images/firefox/enable.png differ diff --git a/app/plugins/bookmarks/images/firefox/export.png b/app/plugins/bookmarks/images/firefox/export.png new file mode 100644 index 0000000..a95ca72 Binary files /dev/null and b/app/plugins/bookmarks/images/firefox/export.png differ diff --git a/app/plugins/bookmarks/images/firefox/extensions-icon.png b/app/plugins/bookmarks/images/firefox/extensions-icon.png new file mode 100644 index 0000000..f364eda Binary files /dev/null and b/app/plugins/bookmarks/images/firefox/extensions-icon.png differ diff --git a/app/plugins/bookmarks/images/firefox/firefox-login.jpg b/app/plugins/bookmarks/images/firefox/firefox-login.jpg new file mode 100644 index 0000000..df62908 Binary files /dev/null and b/app/plugins/bookmarks/images/firefox/firefox-login.jpg differ diff --git a/app/plugins/bookmarks/images/firefox/firefox-save.jpg b/app/plugins/bookmarks/images/firefox/firefox-save.jpg new file mode 100644 index 0000000..48e3a98 Binary files /dev/null and b/app/plugins/bookmarks/images/firefox/firefox-save.jpg differ diff --git a/app/plugins/bookmarks/images/firefox/firefox-settings.jpg b/app/plugins/bookmarks/images/firefox/firefox-settings.jpg new file mode 100644 index 0000000..f2c8921 Binary files /dev/null and b/app/plugins/bookmarks/images/firefox/firefox-settings.jpg differ diff --git a/app/plugins/bookmarks/images/firefox/icon.png b/app/plugins/bookmarks/images/firefox/icon.png new file mode 100644 index 0000000..8e4f9b8 Binary files /dev/null and b/app/plugins/bookmarks/images/firefox/icon.png differ diff --git a/app/plugins/bookmarks/images/firefox/import-export.png b/app/plugins/bookmarks/images/firefox/import-export.png new file mode 100644 index 0000000..5809cc1 Binary files /dev/null and b/app/plugins/bookmarks/images/firefox/import-export.png differ diff --git a/app/plugins/bookmarks/images/firefox/import.png b/app/plugins/bookmarks/images/firefox/import.png new file mode 100644 index 0000000..de7fdf0 Binary files /dev/null and b/app/plugins/bookmarks/images/firefox/import.png differ diff --git a/app/plugins/bookmarks/images/firefox/manage-bookmarks.png b/app/plugins/bookmarks/images/firefox/manage-bookmarks.png new file mode 100644 index 0000000..c0b76a6 Binary files /dev/null and b/app/plugins/bookmarks/images/firefox/manage-bookmarks.png differ diff --git a/app/plugins/bookmarks/images/firefox/manage-page.png b/app/plugins/bookmarks/images/firefox/manage-page.png new file mode 100644 index 0000000..d8c972b Binary files /dev/null and b/app/plugins/bookmarks/images/firefox/manage-page.png differ diff --git a/app/plugins/bookmarks/images/firefox/manage-select.png b/app/plugins/bookmarks/images/firefox/manage-select.png new file mode 100644 index 0000000..b0914de Binary files /dev/null and b/app/plugins/bookmarks/images/firefox/manage-select.png differ diff --git a/app/plugins/bookmarks/images/firefox/manage.png b/app/plugins/bookmarks/images/firefox/manage.png new file mode 100644 index 0000000..d35d783 Binary files /dev/null and b/app/plugins/bookmarks/images/firefox/manage.png differ diff --git a/app/plugins/bookmarks/images/firefox/options.png b/app/plugins/bookmarks/images/firefox/options.png new file mode 100644 index 0000000..234ed54 Binary files /dev/null and b/app/plugins/bookmarks/images/firefox/options.png differ diff --git a/app/plugins/bookmarks/images/firefox/pin.png b/app/plugins/bookmarks/images/firefox/pin.png new file mode 100644 index 0000000..5f35ade Binary files /dev/null and b/app/plugins/bookmarks/images/firefox/pin.png differ diff --git a/app/plugins/bookmarks/images/firefox/pinned.png b/app/plugins/bookmarks/images/firefox/pinned.png new file mode 100644 index 0000000..9f10d05 Binary files /dev/null and b/app/plugins/bookmarks/images/firefox/pinned.png differ diff --git a/app/plugins/bookmarks/images/firefox/settings-page.png b/app/plugins/bookmarks/images/firefox/settings-page.png new file mode 100644 index 0000000..c6c6368 Binary files /dev/null and b/app/plugins/bookmarks/images/firefox/settings-page.png differ diff --git a/app/plugins/bookmarks/images/firefox/settings.png b/app/plugins/bookmarks/images/firefox/settings.png new file mode 100644 index 0000000..5af44af Binary files /dev/null and b/app/plugins/bookmarks/images/firefox/settings.png differ diff --git a/app/plugins/bookmarks/images/firefox/version.png b/app/plugins/bookmarks/images/firefox/version.png new file mode 100644 index 0000000..ed7e94e Binary files /dev/null and b/app/plugins/bookmarks/images/firefox/version.png differ diff --git a/app/plugins/bookmarks/images/opera/bookmarks.png b/app/plugins/bookmarks/images/opera/bookmarks.png new file mode 100644 index 0000000..6d61869 Binary files /dev/null and b/app/plugins/bookmarks/images/opera/bookmarks.png differ diff --git a/app/plugins/bookmarks/images/opera/export.png b/app/plugins/bookmarks/images/opera/export.png new file mode 100644 index 0000000..afee201 Binary files /dev/null and b/app/plugins/bookmarks/images/opera/export.png differ diff --git a/app/plugins/bookmarks/images/opera/extensions.png b/app/plugins/bookmarks/images/opera/extensions.png new file mode 100644 index 0000000..48587bc Binary files /dev/null and b/app/plugins/bookmarks/images/opera/extensions.png differ diff --git a/app/plugins/bookmarks/images/opera/import-select.png b/app/plugins/bookmarks/images/opera/import-select.png new file mode 100644 index 0000000..ce92936 Binary files /dev/null and b/app/plugins/bookmarks/images/opera/import-select.png differ diff --git a/app/plugins/bookmarks/images/opera/import-start.png b/app/plugins/bookmarks/images/opera/import-start.png new file mode 100644 index 0000000..75b2a2c Binary files /dev/null and b/app/plugins/bookmarks/images/opera/import-start.png differ diff --git a/app/plugins/bookmarks/images/opera/import-success.png b/app/plugins/bookmarks/images/opera/import-success.png new file mode 100644 index 0000000..e34bc70 Binary files /dev/null and b/app/plugins/bookmarks/images/opera/import-success.png differ diff --git a/app/plugins/bookmarks/images/opera/import.png b/app/plugins/bookmarks/images/opera/import.png new file mode 100644 index 0000000..02c0046 Binary files /dev/null and b/app/plugins/bookmarks/images/opera/import.png differ diff --git a/app/plugins/bookmarks/images/opera/opera-login.jpg b/app/plugins/bookmarks/images/opera/opera-login.jpg new file mode 100644 index 0000000..0bb1bc7 Binary files /dev/null and b/app/plugins/bookmarks/images/opera/opera-login.jpg differ diff --git a/app/plugins/bookmarks/images/opera/opera-promo-image.png b/app/plugins/bookmarks/images/opera/opera-promo-image.png new file mode 100644 index 0000000..cd2ca2a Binary files /dev/null and b/app/plugins/bookmarks/images/opera/opera-promo-image.png differ diff --git a/app/plugins/bookmarks/images/opera/opera-save.jpg b/app/plugins/bookmarks/images/opera/opera-save.jpg new file mode 100644 index 0000000..aaa5aa6 Binary files /dev/null and b/app/plugins/bookmarks/images/opera/opera-save.jpg differ diff --git a/app/plugins/bookmarks/images/opera/opera-settiings.jpg b/app/plugins/bookmarks/images/opera/opera-settiings.jpg new file mode 100644 index 0000000..37c4081 Binary files /dev/null and b/app/plugins/bookmarks/images/opera/opera-settiings.jpg differ diff --git a/app/plugins/bookmarks/images/opera/optiions.png b/app/plugins/bookmarks/images/opera/optiions.png new file mode 100644 index 0000000..1e0f192 Binary files /dev/null and b/app/plugins/bookmarks/images/opera/optiions.png differ diff --git a/app/plugins/bookmarks/images/opera/options.png b/app/plugins/bookmarks/images/opera/options.png new file mode 100644 index 0000000..1e0f192 Binary files /dev/null and b/app/plugins/bookmarks/images/opera/options.png differ diff --git a/app/plugins/bookmarks/images/opera/pin.png b/app/plugins/bookmarks/images/opera/pin.png new file mode 100644 index 0000000..986aac1 Binary files /dev/null and b/app/plugins/bookmarks/images/opera/pin.png differ diff --git a/app/plugins/bookmarks/images/opera/pinned.png b/app/plugins/bookmarks/images/opera/pinned.png new file mode 100644 index 0000000..4881f6c Binary files /dev/null and b/app/plugins/bookmarks/images/opera/pinned.png differ diff --git a/app/plugins/bookmarks/images/opera/settings-page.png b/app/plugins/bookmarks/images/opera/settings-page.png new file mode 100644 index 0000000..9103530 Binary files /dev/null and b/app/plugins/bookmarks/images/opera/settings-page.png differ diff --git a/app/plugins/bookmarks/images/opera/sidebar.png b/app/plugins/bookmarks/images/opera/sidebar.png new file mode 100644 index 0000000..bc0a3d6 Binary files /dev/null and b/app/plugins/bookmarks/images/opera/sidebar.png differ diff --git a/app/plugins/bookmarks/images/opera/toolbar.png b/app/plugins/bookmarks/images/opera/toolbar.png new file mode 100644 index 0000000..6a84ab2 Binary files /dev/null and b/app/plugins/bookmarks/images/opera/toolbar.png differ diff --git a/app/plugins/bookmarks/js/bookmarklet.js b/app/plugins/bookmarks/js/bookmarklet.js new file mode 100644 index 0000000..df5b86c --- /dev/null +++ b/app/plugins/bookmarks/js/bookmarklet.js @@ -0,0 +1,35 @@ +javascript:(function() { + const apiKey = localStorage.getItem('notAnAuthToken'); + const apiUrl = localStorage.getItem('api_url'); + const name = prompt("Enter a name for the bookmark:"); + const notes = prompt("Enter any notes (optional):"); + const color = prompt("Enter a color (optional):"); + const privacy = prompt("Enter privacy level (e.g., public/private):"); + const folder = prompt("Enter a folder (optional):"); + const url = window.location.href; + + if (!name) { + alert("Name is required."); + return; + } + + fetch(apiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${apiKey}` + }, + body: JSON.stringify({ name, url, notes, color, privacy, folder }) + }) + .then(response => { + if (response.ok) { + alert("Bookmark saved successfully!"); + } else { + alert("Failed to save bookmark. Please check your API key."); + } + }) + .catch(error => { + console.error(error); + alert("An unknown error occurred while saving the bookmark."); + }); +})(); diff --git a/app/plugins/bookmarks/js/bookmarks.js b/app/plugins/bookmarks/js/bookmarks.js new file mode 100644 index 0000000..b73a23c --- /dev/null +++ b/app/plugins/bookmarks/js/bookmarks.js @@ -0,0 +1,234 @@ +let masonryInstance; +document.addEventListener('DOMContentLoaded', () => { + // Handle all bootstrap popover inits + const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]') + const popoverList = [...popoverTriggerList].map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl)) + + // Initialize all toggle functions + toggleVisibility('editModeSwitch', 'edit-mode'); + toggleVisibility('showArchivedSwitch', 'link-archived'); + toggleVisibility('showHiddenSwitch', 'link-hidden'); + toggleVisibility('archiveButtonSwitch', 'btn-archive'); + toggleVisibility('visibilityButtonSwitch', 'btn-hideshow'); + toggleVisibility('privacyButtonSwitch', 'btn-publish'); + toggleVisibility('addButtonSwitch', 'btn-addlink'); + toggleVisibility('shareButtonSwitch', 'btn-share'); + + toggleVisibility('dashShowArchivedSwitch', 'link-archived'); + toggleVisibility('dashShowHiddenSwitch', 'link-hidden'); + toggleVisibility('dashAddButtonSwitch', 'btn-addlink'); + toggleVisibility('dashShareButtonSwitch', 'btn-share'); + + // Retrieve the list of collapsed folderCard IDs from local storage + + const onDashboard = document.getElementById("dash_id"); + let collapsedFolders; + if ( onDashboard ) { + collapsedFolders = JSON.parse(localStorage.getItem( 'collapsedDashFolders' + onDashboard.value )) || []; + } else { + collapsedFolders = JSON.parse(localStorage.getItem( 'collapsedFolders' )) || []; + } + + // Collapse the elements stored in local storage when the page loads + collapsedFolders.forEach((folderId) => { + const collapseElement = document.querySelector(`#Collapse${folderId}`); + if ( collapseElement ) { + collapseElement.classList.remove('show'); + } + }); + + // Add event listeners to track expand and collapse actions + document.querySelectorAll('.accordion-item').forEach((item) => { + const folderCardId = item.closest('.bookmark-card')?.id?.replace('folderCard', ''); + + if ( ! folderCardId ) return; + + const collapseElement = item.querySelector('.collapse'); + + // Listen for collapse and expand events + collapseElement.addEventListener('hidden.bs.collapse', () => { + let storageName; + // Add the folderCard ID to local storage when collapsed + if (!collapsedFolders.includes(folderCardId)) { + collapsedFolders.push(folderCardId); + if ( onDashboard ) { + storageName = 'collapsedDashFolders' + onDashboard.value; + } else { + storageName = 'collapsedFolders'; + } + localStorage.setItem( storageName , JSON.stringify(collapsedFolders)); + } + }); + + collapseElement.addEventListener('shown.bs.collapse', () => { + let storageName; + // Remove the folderCard ID from local storage when expanded + const index = collapsedFolders.indexOf(folderCardId); + if (index > -1) { + collapsedFolders.splice(index, 1); + if ( onDashboard ) { + storageName = 'collapsedDashFolders' + onDashboard.value; + } else { + storageName = 'collapsedFolders'; + } + localStorage.setItem( storageName , JSON.stringify(collapsedFolders)); + } + }); + }); + + const masonryContainer = document.getElementById("bookmarkSort"); + + if ( ! masonryContainer ) { + return; + } + + if ( localStorage.getItem('manageFolderOrder') ) { + loadDashLinkOrder(); + } + + masonryInstance = new Masonry(masonryContainer, { + columnHeight: '.accordion', + itemSelector: ".bookmark-card", + percentPosition: false + }); + + const test = () => masonryInstance.layout(); + + masonryContainer.addEventListener('hidden.bs.collapse', test ); + masonryContainer.addEventListener('shown.bs.collapse', test ); + + // Select all folder-raise and folder-lower buttons + + const raiseButtons = document.querySelectorAll(".folder-raise"); + const lowerButtons = document.querySelectorAll(".folder-lower"); + + // Function to move the folderCard up + function moveUp(event) { + const folderCard = event.target.closest(".bookmark-card"); + const bookmarkSort = document.getElementById("bookmarkSort"); + const orderId = document.getElementById("dashLinkOrder"); + + if (folderCard) { + const previousSibling = folderCard.previousElementSibling; + if (previousSibling) { + // Remove and reinsert the element before the previous sibling + bookmarkSort.removeChild(folderCard); + bookmarkSort.insertBefore(folderCard, previousSibling); + masonryInstance.reloadItems(); + masonryInstance.layout(); + updateDashLinkOrder(); + } + } + } + + // Function to move the folderCard down + function moveDown(event) { + const folderCard = event.target.closest(".bookmark-card"); + const bookmarkSort = document.getElementById("bookmarkSort"); + + if (folderCard) { + const nextSibling = folderCard.nextElementSibling; + if (nextSibling) { + // Remove and reinsert the element after the next sibling + bookmarkSort.removeChild(folderCard); + bookmarkSort.insertBefore(folderCard, nextSibling.nextSibling); + masonryInstance.reloadItems(); + masonryInstance.layout(); + updateDashLinkOrder(); + } + } + } + + // Add event listeners to the raise buttons + raiseButtons.forEach(button => { + button.addEventListener("click", moveUp); + }); + + // Add event listeners to the lower buttons + lowerButtons.forEach(button => { + button.addEventListener("click", moveDown); + }); + // after hiding/showing elements + masonryInstance.layout(); +}); + +// Function to handle showing or hiding elements based on the checkbox state +function toggleVisibility(switchId, className) { + const switchElement = document.getElementById(switchId); + const elementsToToggle = document.querySelectorAll(`.${className}`); + + if ( ! switchElement ) { + return; + } + + // Listen for changes to the checkbox + switchElement.addEventListener('change', () => { + if (switchElement.checked) { + elementsToToggle.forEach(element => { + element.style.display = ''; + }); + } else { + elementsToToggle.forEach(element => { + element.style.display = 'none'; + }); + } + if (typeof masonryInstance !== 'undefined') { + masonryInstance.reloadItems(); + masonryInstance.layout() + return; + } + }); + + // Trigger the toggle initially to set the correct visibility on page load + switchElement.dispatchEvent(new Event('change')); +} + +function updateDashLinkOrder() { + const bookmarkCards = document.querySelectorAll("#bookmarkSort .bookmark-card"); + const ids = Array.from( bookmarkCards ).map( card => { + const match = card.id.match(/folderCard(\d+)/); + return match ? match[1] : null; + }).filter( id => id !== null ); + + const dashLinkOrder = document.getElementById("dashLinkOrder"); + if (dashLinkOrder) { + dashLinkOrder.value = ids.join(","); + } else { + localStorage.setItem('manageFolderOrder', ids.join(",")); + } +} + +function loadDashLinkOrder() { + const onDashboard = document.getElementById("dash_id"); + const storedOrder = localStorage.getItem("manageFolderOrder"); // Get the saved order + const bookmarkSort = document.getElementById("bookmarkSort"); + + if ( onDashboard || !storedOrder || !bookmarkSort ) return; // Exit if no saved order or no container + + const orderArray = storedOrder.split(","); // Convert the saved order into an array + const bookmarkCards = Array.from(document.querySelectorAll("#bookmarkSort .bookmark-card")); + + // Create a map for quick lookup of cards by their ID + const cardMap = new Map(); + bookmarkCards.forEach(card => { + const match = card.id.match(/folderCard(\d+)/); + if (match) cardMap.set(match[1], card); + }); + + // Reorder elements based on the saved order + const orderedElements = []; + orderArray.forEach(id => { + if (cardMap.has(id)) { + orderedElements.push(cardMap.get(id)); // Add elements in the specified order + cardMap.delete(id); // Remove from the map once processed + } + }); + + // Add any remaining (unspecified) elements to the end of the list + const remainingElements = Array.from(cardMap.values()); + orderedElements.push(...remainingElements); + + // Clear the container and append the elements in the new order + bookmarkSort.innerHTML = ""; // Remove all children + orderedElements.forEach(element => bookmarkSort.appendChild(element)); // Append in order +} diff --git a/app/plugins/bookmarks/models/bookmark_dashboards.php b/app/plugins/bookmarks/models/bookmark_dashboards.php new file mode 100644 index 0000000..a652af9 --- /dev/null +++ b/app/plugins/bookmarks/models/bookmark_dashboards.php @@ -0,0 +1,176 @@ + + * @link https://TheTempusProject.com + * @license https://opensource.org/licenses/MIT [MIT LICENSE] + */ +namespace TheTempusProject\Models; + +use TheTempusProject\Bedrock\Functions\Check; +use TheTempusProject\Canary\Bin\Canary as Debug; +use TheTempusProject\Classes\DatabaseModel; +use TheTempusProject\TheTempusProject as App; +use TheTempusProject\Houdini\Classes\Filters; +use TheTempusProject\Canary\Classes\CustomException; + +class BookmarkDashboards extends DatabaseModel { + public $tableName = 'bookmark_dashboards'; + public $databaseMatrix = [ + [ 'title', 'varchar', '256' ], + [ 'saved_prefs', 'text', '' ], + [ 'link_order', 'text', '' ], + [ 'description', 'text', '' ], + [ 'createdBy', 'int', '11' ], + [ 'createdAt', 'int', '11' ], + [ 'updatedAt', 'int', '11' ], + [ 'uuid', 'char', '36' ], + ]; + + /** + * The model constructor. + */ + public function __construct() { + parent::__construct(); + } + + public function create( $title, $saved_prefs, $link_order, $description = '' ) { + if ( ! Check::dataTitle( $title ) ) { + Debug::info( 'Dash: illegal title.' ); + return false; + } + $fields = [ + 'title' => $title, + 'description' => $description, + 'saved_prefs' => $saved_prefs, + 'link_order' => $link_order, + 'uuid' => generateUuidV4(), + 'createdBy' => App::$activeUser->ID, + 'createdAt' => time(), + ]; + if ( ! self::$db->insert( $this->tableName, $fields ) ) { + new CustomException( 'dashCreate' ); + Debug::error( "Dash: not created " . var_export($fields,true) ); + return false; + } + return self::$db->lastId(); + } + + public function update( $id, $title, $saved_prefs, $link_order, $description = '' ) { + if ( !Check::id( $id ) ) { + Debug::info( 'Dash: illegal ID.' ); + return false; + } + if ( !Check::dataTitle( $title ) ) { + Debug::info( 'Dash: illegal title.' ); + return false; + } + $fields = [ + 'title' => $title, + 'description' => $description, + 'saved_prefs' => $saved_prefs, + 'link_order' => $link_order, + ]; + if ( !self::$db->update( $this->tableName, $id, $fields ) ) { + new CustomException( 'dashUpdate' ); + Debug::error( "Dash: $id not updated" ); + return false; + } + return true; + } + + public function updateDash( $id, $saved_prefs = '', $link_order = '' ) { + if ( !Check::id( $id ) ) { + Debug::info( 'Dash: illegal ID.' ); + return false; + } + $fields = []; + $fields['saved_prefs'] = $saved_prefs; + if ( ! empty( $link_order ) ) { + $fields['link_order'] = $link_order; + } + if ( empty( $fields ) ) { + return true; + } + if ( !self::$db->update( $this->tableName, $id, $fields ) ) { + new CustomException( 'dashUpdate' ); + Debug::error( "Dash: $id not updated" ); + return false; + } + return true; + } + + public function byUser( $limit = null ) { + $whereClause = ['createdBy', '=', App::$activeUser->ID]; + if ( empty( $limit ) ) { + $views = self::$db->get( $this->tableName, $whereClause ); + } else { + $views = self::$db->get( $this->tableName, $whereClause, 'ID', 'DESC', [0, $limit] ); + } + if ( !$views->count() ) { + Debug::info( 'No Views found.' ); + return false; + } + return $this->filter( $views->results() ); + } + + public function getName( $id ) { + $views = self::findById( $id ); + if (false == $views) { + return 'unknown'; + } + return $views->title; + } + + public function simpleByUser() { + $whereClause = ['createdBy', '=', App::$activeUser->ID]; + $views = self::$db->get( $this->tableName, $whereClause ); + if ( !$views->count() ) { + Debug::warn( 'Could not find any Views' ); + return false; + } + + $views = $views->results(); + $out = []; + foreach ( $views as $view ) { + $out[ $view->title ] = $view->ID; + } + return $out; + } + + public function simpleObjectByUser() { + $whereClause = ['createdBy', '=', App::$activeUser->ID]; + $views = self::$db->get( $this->tableName, $whereClause ); + if ( !$views->count() ) { + Debug::warn( 'Could not find any Views' ); + return false; + } + + $views = $views->results(); + $out = []; + foreach ( $views as $view ) { + $obj = new \stdClass(); + $obj->title = $view->title; + $obj->ID = $view->ID; + $out[] = $obj; + } + return $out; + } + + public function findByUuid( $id ) { + $whereClause = ['uuid', '=', $id]; + + $dashboards = self::$db->get( $this->tableName, $whereClause ); + + if ( !$dashboards->count() ) { + Debug::info( 'No Dashboards found.' ); + return false; + } + return $this->filter( $dashboards->first() ); + } +} diff --git a/app/plugins/bookmarks/models/bookmarks.php b/app/plugins/bookmarks/models/bookmarks.php new file mode 100644 index 0000000..80b0277 --- /dev/null +++ b/app/plugins/bookmarks/models/bookmarks.php @@ -0,0 +1,800 @@ + + * @link https://TheTempusProject.com + * @license https://opensource.org/licenses/MIT [MIT LICENSE] + */ +namespace TheTempusProject\Models; + +use TheTempusProject\Bedrock\Functions\Check; +use TheTempusProject\Canary\Bin\Canary as Debug; +use TheTempusProject\Classes\DatabaseModel; +use TheTempusProject\TheTempusProject as App; +use TheTempusProject\Houdini\Classes\Filters; +use TheTempusProject\Canary\Classes\CustomException; + +class Bookmarks extends DatabaseModel { + public $tableName = 'bookmarks'; + public $linkTypes = [ + 'Open in New Tab' => 'external', + 'Open in Same Tab' => 'internal', + ]; + + public $databaseMatrix = [ + [ 'title', 'varchar', '256' ], + [ 'url', 'text', '' ], + [ 'color', 'varchar', '48' ], + [ 'privacy', 'varchar', '48' ], + [ 'folderID', 'int', '11' ], + [ 'description', 'text', '' ], + [ 'createdBy', 'int', '11' ], + [ 'createdAt', 'int', '11' ], + [ 'meta', 'text', '' ], + [ 'icon', 'text', '' ], + [ 'archivedAt', 'int', '11' ], + [ 'refreshedAt', 'int', '11' ], + [ 'hiddenAt', 'int', '11' ], + [ 'order', 'int', '11' ], + [ 'linkType', 'varchar', '32' ], + [ 'uuid', 'char', '36' ], + ]; + + /** + * The model constructor. + */ + public function __construct() { + parent::__construct(); + } + + public function create( $title, $url, $folderID = 0, $description = '', $color = 'default', $privacy = 'private', $type = 'external', $user = null ) { + if ( empty( $user ) ) { + $user = App::$activeUser->ID; + } + $fields = [ + 'title' => $title, + 'url' => $url, + 'description' => $description, + 'color' => $color, + 'privacy' => $privacy, + 'createdBy' => $user, + 'uuid' => generateUuidV4(), + 'createdAt' => time(), + ]; + if ( !empty( $folderID ) ) { + $fields['folderID'] = $folderID; + } else { + $fields['folderID'] = null; + } + if ( ! self::$db->insert( $this->tableName, $fields ) ) { + new CustomException( 'bookmarkCreate' ); + Debug::error( "Bookmarks: not created " . var_export($fields,true) ); + return false; + } + return self::$db->lastId(); + } + + public function update( $id, $title, $url, $folderID = 0, $description = '', $color = 'default', $privacy = 'private', $type = 'external', $order = 0 ) { + if ( !Check::id( $id ) ) { + Debug::info( 'Bookmarks: illegal ID.' ); + return false; + } + $fields = [ + 'title' => $title, + 'url' => $url, + 'description' => $description, + 'color' => $color, + 'privacy' => $privacy, + // 'linkType' => $type, + // 'order' => $order, + ]; + if ( !empty( $folderID ) ) { + $fields['folderID'] = $folderID; + } + if ( !self::$db->update( $this->tableName, $id, $fields ) ) { + new CustomException( 'bookmarkUpdate' ); + Debug::error( "Bookmarks: $id not updated" ); + return false; + } + return true; + } + + public function findByUuid( $id ) { + $whereClause = ['uuid', '=', $id]; + if ( empty( $limit ) ) { + $bookmarks = self::$db->get( $this->tableName, $whereClause ); + } else { + $bookmarks = self::$db->get( $this->tableName, $whereClause, 'ID', 'DESC', [0, $limit] ); + } + if ( !$bookmarks->count() ) { + Debug::info( 'No Bookmarks found.' ); + return false; + } + return $this->filter( $bookmarks->first() ); + } + + public function byUser( $limit = null ) { + $whereClause = ['createdBy', '=', App::$activeUser->ID]; + if ( empty( $limit ) ) { + $bookmarks = self::$db->get( $this->tableName, $whereClause ); + } else { + $bookmarks = self::$db->get( $this->tableName, $whereClause, 'ID', 'DESC', [0, $limit] ); + } + if ( !$bookmarks->count() ) { + Debug::info( 'No Bookmarks found.' ); + return false; + } + return $this->filter( $bookmarks->results() ); + } + + public function unsafeByFolder( $id, $limit = null ) { + $whereClause = [ 'folderID', '=', $id ]; + if ( empty( $limit ) ) { + $bookmarks = self::$db->get( $this->tableName, $whereClause ); + } else { + $bookmarks = self::$db->get( $this->tableName, $whereClause, 'ID', 'DESC', [0, $limit] ); + } + if ( !$bookmarks->count() ) { + Debug::info( 'No Bookmarks found.' ); + return false; + } + return $this->filter( $bookmarks->results() ); + } + + public function byFolder( $id, $hiddenExcluded = false, $archivedExcluded = false ) { + $whereClause = ['createdBy', '=', App::$activeUser->ID ]; + $whereClause = array_merge( $whereClause, [ 'AND', 'folderID', '=', $id ] ); + if ( ! empty( $hiddenExcluded ) ) { + $whereClause = array_merge( $whereClause, [ 'AND', 'hiddenAt', 'is', 'NULL'] ); + } + if ( ! empty( $archivedExcluded ) ) { + $whereClause = array_merge( $whereClause, [ 'AND', 'archivedAt', 'is', 'NULL'] ); + } + $bookmarks = self::$db->get( $this->tableName, $whereClause ); + if ( ! $bookmarks->count() ) { + Debug::info( 'No Bookmarks found.' ); + return false; + } + return $this->filter( $bookmarks->results() ); + } + + public function publicByFolder( $id, $limit = null ) { + $whereClause = ['createdBy', '=', App::$activeUser->ID, 'AND']; + $whereClause = array_merge( $whereClause, [ 'folderID', '=', $id, 'AND', 'privacy', '=', 'public' ] ); + if ( empty( $limit ) ) { + $bookmarks = self::$db->get( $this->tableName, $whereClause ); + } else { + $bookmarks = self::$db->get( $this->tableName, $whereClause, 'ID', 'DESC', [0, $limit] ); + } + if ( !$bookmarks->count() ) { + Debug::info( 'No Bookmarks found.' ); + return false; + } + return $this->filter( $bookmarks->results() ); + } + + public function noFolder( $id = 0, $limit = 10 ) { + $whereClause = ['createdBy', '=', App::$activeUser->ID, 'AND']; + if ( !empty( $id ) ) { + $whereClause = array_merge( $whereClause, ['folderID', '!=', $id] ); + } else { + $whereClause = array_merge( $whereClause, [ 'folderID', 'IS', null] ); + } + if ( empty( $limit ) ) { + $bookmarks = self::$db->get( $this->tableName, $whereClause ); + } else { + $bookmarks = self::$db->get( $this->tableName, $whereClause, 'ID', 'DESC', [ 0, $limit ] ); + } + if ( !$bookmarks->count() ) { + Debug::info( 'No Bookmarks found.' ); + return false; + } + return $this->filter( $bookmarks->results() ); + } + + public function getName( $id ) { + $bookmarks = self::findById( $id ); + if (false == $bookmarks) { + return 'unknown'; + } + return $bookmarks->title; + } + + public function getColor( $id ) { + $bookmarks = self::findById( $id ); + if (false == $bookmarks) { + return 'default'; + } + return $bookmarks->color; + } + + public function simpleByUser() { + $whereClause = ['createdBy', '=', App::$activeUser->ID]; + $bookmarks = self::$db->get( $this->tableName, $whereClause ); + if ( !$bookmarks->count() ) { + Debug::warn( 'Could not find any bookmarks' ); + return false; + } + + $bookmarks = $bookmarks->results(); + $out = []; + foreach ( $bookmarks as $bookmarks ) { + $out[ $bookmarks->title ] = $bookmarks->ID; + } + return $out; + } + + public function simpleObjectByUser() { + $whereClause = ['createdBy', '=', App::$activeUser->ID]; + $bookmarks = self::$db->get( $this->tableName, $whereClause ); + if ( !$bookmarks->count() ) { + Debug::warn( 'Could not find any bookmarks' ); + return false; + } + + $bookmarks = $bookmarks->results(); + $out = []; + foreach ( $bookmarks as $bookmarks ) { + $obj = new \stdClass(); + $obj->title = $bookmarks->title; + $obj->ID = $bookmarks->ID; + $out[] = $obj; + } + return $out; + } + + public function deleteByFolder( $folderID ) { + $whereClause = [ 'createdBy', '=', App::$activeUser->ID, 'AND' ]; + $whereClause = array_merge( $whereClause, [ 'folderID', '=', $folderID ] ); + $bookmarks = self::$db->get( $this->tableName, $whereClause ); + if ( ! $bookmarks->count() ) { + Debug::info( 'No ' . $this->tableName . ' data found.' ); + return []; + } + foreach( $bookmarks->results() as $bookmark ) { + $this->delete( $bookmark->ID ); + } + return true; + } + + private function resolveShortenedUrl( $url ) { + $ch = curl_init($url); + + // Set curl options + curl_setopt($ch, CURLOPT_NOBODY, true); // We don't need the body + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); // Follow redirects + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // Return the response + curl_setopt($ch, CURLOPT_TIMEOUT, 30); // Set a timeout + // curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); // Maybe sketchy? + // Maybe sketchy? + // curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); // Disable SSL host verification + // curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // Disable SSL peer verification + // curl_setopt($ch, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2); + + // added to support the regex site + // curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); + + // = + // curl_setopt($ch, CURLOPT_SSL_CIPHER_LIST, 'AES256+EECDH:AES256+EDH' ); + + + + + // Execute curl + $response = curl_exec( $ch ); + + // Check if there was an error + if ( curl_errno( $ch ) ) { + + + // Get error details + $errorCode = curl_errno($ch); + $errorMessage = curl_error($ch); + // Log or display the error details + dv('cURL Error: ' . $errorMessage . ' (Error Code: ' . $errorCode . ')'); + + curl_close($ch); + // return $url; // Return the original URL if there was an error + + + $url = rtrim( $url, '/' ); + $ch2 = curl_init($url); + curl_setopt($ch2, CURLOPT_NOBODY, true); // We don't need the body + curl_setopt($ch2, CURLOPT_FOLLOWLOCATION, true); // Follow redirects + curl_setopt($ch2, CURLOPT_RETURNTRANSFER, true); // Return the response + curl_setopt($ch2, CURLOPT_TIMEOUT, 5); // Set a timeout + curl_exec($ch2); + + if ( curl_errno( $ch2 ) ) { + + } + curl_close( $ch ); + return $url; + } + + // Get the effective URL (the final destination after redirects) + $finalUrl = curl_getinfo( $ch, CURLINFO_EFFECTIVE_URL ); + curl_close( $ch ); + + return $finalUrl; + + // $headers = get_headers( $url, 1 ); + $headers = @get_headers($url, 1); + if ( $headers === false ) { + } + if ( isset( $headers['Location'] ) ) { + if (is_array($headers['Location'])) { + return end($headers['Location']); + } else { + return $headers['Location']; + } + } else { + return $url; + } + } + + public function filter( $data, $params = [] ) { + foreach ( $data as $instance ) { + if ( !is_object( $instance ) ) { + $instance = $data; + $end = true; + } + + $instance->iconHtml = ''; + if ( ! empty( $instance->icon ) ) { + if ( strpos($instance->icon, 'http') !== false) { + $instance->iconHtml = ''; + } else { + if ( ! empty( $instance->url ) ) { + $base_url = $this->getBaseUrl( $instance->url ); + $instance->iconHtml = ''; + } + } + } + + if ( $instance->privacy == 'private' ) { + $instance->privacyBadge = 'Private'; + $instance->publish = ' + + + '; + } else { + $instance->privacyBadge = 'Public'; + $instance->publish = ' + + + '; + } + if ( empty( $instance->hiddenAt ) ) { + $instance->hidden_class = ''; + $instance->hideBtn = ' + + + '; + } else { + $instance->hidden_class = 'link-hidden'; + $instance->hideBtn = ' + + + '; + } + + if ( empty( $instance->archivedAt ) ) { + $instance->archived_class = ''; + $instance->archiveBtn = ' + + + '; + } else { + $instance->archived_class = 'link-archived'; + $instance->archiveBtn = ' + + + '; + } + + if ( ! empty( $instance->refreshedAt ) && time() < ( $instance->refreshedAt + ( 60 * 10 ) ) ) { + $instance->refreshBtn = ' + + + '; + } else { + $instance->refreshBtn = ' + + + '; + } + + $out[] = $instance; + if ( !empty( $end ) ) { + $out = $out[0]; + break; + } + } + return $out; + } + + public function hide( $id ) { + if ( !Check::id( $id ) ) { + Debug::info( 'Bookmarks: illegal ID.' ); + return false; + } + $fields = [ + 'hiddenAt' => time(), + ]; + if ( !self::$db->update( $this->tableName, $id, $fields ) ) { + new CustomException( 'bookmarkUpdate' ); + Debug::error( "Bookmarks: $id not updated" ); + return false; + } + return true; + } + + public function show( $id ) { + if ( !Check::id( $id ) ) { + Debug::info( 'Bookmarks: illegal ID.' ); + return false; + } + $fields = [ + 'hiddenAt' => NULL, + ]; + if ( !self::$db->update( $this->tableName, $id, $fields ) ) { + new CustomException( 'bookmarkUpdate' ); + Debug::error( "Bookmarks: $id not updated" ); + return false; + } + return true; + } + + public function publish( $id ) { + if ( !Check::id( $id ) ) { + Debug::info( 'Bookmarks: illegal ID.' ); + return false; + } + $fields = [ + 'privacy' => 'public', + ]; + if ( !self::$db->update( $this->tableName, $id, $fields ) ) { + new CustomException( 'bookmarkUpdate' ); + Debug::error( "Bookmarks: $id not updated" ); + return false; + } + return true; + } + + public function retract( $id ) { + if ( !Check::id( $id ) ) { + Debug::info( 'Bookmarks: illegal ID.' ); + return false; + } + $fields = [ + 'privacy' => 'private', + ]; + if ( !self::$db->update( $this->tableName, $id, $fields ) ) { + new CustomException( 'bookmarkUpdate' ); + Debug::error( "Bookmarks: $id not updated" ); + return false; + } + return true; + } + + public function archive( $id ) { + if ( !Check::id( $id ) ) { + Debug::info( 'Bookmarks: illegal ID.' ); + return false; + } + $fields = [ + 'archivedAt' => time(), + ]; + if ( !self::$db->update( $this->tableName, $id, $fields ) ) { + new CustomException( 'bookmarkUpdate' ); + Debug::error( "Bookmarks: $id not updated" ); + return false; + } + return true; + } + + public function unarchive( $id ) { + if ( !Check::id( $id ) ) { + Debug::info( 'Bookmarks: illegal ID.' ); + return false; + } + $fields = [ + 'archivedAt' => NULL, + ]; + if ( !self::$db->update( $this->tableName, $id, $fields ) ) { + new CustomException( 'bookmarkUpdate' ); + Debug::error( "Bookmarks: $id not updated" ); + return false; + } + return true; + } + + public function extractMetaTags($htmlContent) { + $doc = new \DOMDocument(); + @$doc->loadHTML($htmlContent); + $metaTags = []; + foreach ($doc->getElementsByTagName('meta') as $meta) { + $name = $meta->getAttribute('name') ?: $meta->getAttribute('property'); + $content = $meta->getAttribute('content'); + if ($name && $content) { + $metaTags[$name] = $content; + } + } + return $metaTags; + } + + public function fetchUrlData($url) { + $ch = curl_init(); + + // Set cURL options + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HEADER, true); // Include headers in the output + curl_setopt($ch, CURLOPT_NOBODY, false); // Include the body in the output + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); // Follow redirects + curl_setopt($ch, CURLOPT_TIMEOUT, 30); // Set a timeout + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); // Disable SSL host verification for testing + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // Disable SSL peer verification for testing + + // Execute cURL request + $response = curl_exec($ch); + + // Check if there was an error + if (curl_errno($ch)) { + $errorMessage = curl_error($ch); + curl_close($ch); + throw new \Exception('cURL Error: ' . $errorMessage); + } + + // Get HTTP status code + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + + // Separate headers and body + $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); + $headers = substr($response, 0, $headerSize); + $body = substr($response, $headerSize); + + curl_close($ch); + + // Parse headers into an associative array + $headerLines = explode("\r\n", trim($headers)); + $headerArray = []; + foreach ($headerLines as $line) { + $parts = explode(': ', $line, 2); + if (count($parts) == 2) { + $headerArray[$parts[0]] = $parts[1]; + } + } + + return [ + 'http_code' => $httpCode, + 'headers' => $headerArray, + 'body' => $body, + ]; + } + + private function getMetaTagsAndFavicon( $url ) { + try { + // $url = 'https://runescape.wiki'; + $data = $this->fetchUrlData($url); + + // iv($data); + + // Get headers + $headers = $data['headers']; + iv($headers); + + // Get meta tags + $metaTags = $this->extractMetaTags($data['body']); + } catch (Exception $e) { + dv( 'Error: ' . $e->getMessage()); + + } + $metaInfo = [ + 'url' => $url, + 'title' => null, + 'description' => null, + 'image' => null, + 'favicon' => null + ]; + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + $html = curl_exec($ch); + curl_close($ch); + + if ($html === false) { + return null; + } + + $meta = @get_meta_tags( $url ); + + $dom = new \DOMDocument('1.0', 'utf-8'); + $dom->strictErrorChecking = false; + $dom->loadHTML($html, LIBXML_NOERROR); + $xml = simplexml_import_dom($dom); + $arr = $xml->xpath('//link[@rel="shortcut icon"]'); + + // Get the title of the page + $titles = $dom->getElementsByTagName('title'); + if ($titles->length > 0) { + $metaInfo['title'] = $titles->item(0)->nodeValue; + } + + // Get the meta tags + $metaTags = $dom->getElementsByTagName('meta'); + $metadata = []; + foreach ($metaTags as $meta) { + $metadata[] = [ + 'name' => $meta->getAttribute('name'), + 'property' => $meta->getAttribute('property'), + 'content' => $meta->getAttribute('content') + ]; + if ($meta->getAttribute('name') === 'description') { + $metaInfo['description'] = $meta->getAttribute('content'); + } + if ($meta->getAttribute('itemprop') === 'image') { + $metaInfo['google_image'] = $meta->getAttribute('content'); + } + if ($meta->getAttribute('property') === 'og:image') { + $metaInfo['image'] = $meta->getAttribute('content'); + } + } + + // Get the link tags to find the favicon + $linkTags = $dom->getElementsByTagName('link'); + $metadata['links'] = []; + foreach ($linkTags as $link) { + $metadata['links'][] = [ $link->getAttribute('rel') => $link->getAttribute('href') ]; + if ( $link->getAttribute('rel') === 'icon' || $link->getAttribute('rel') === 'shortcut icon') { + $metaInfo['favicon'] = $link->getAttribute('href'); + break; + } + } + + $metaInfo['metadata'] = $metadata; + + return $metaInfo; + } + + public function retrieveInfo( $url ) { + $finalDestination = $this->resolveShortenedUrl( $url ); + $info = $this->getMetaTagsAndFavicon( $finalDestination ); + $base_url = $this->getBaseUrl( $finalDestination ); + + if ( ! empty( $info['favicon'] ) ) { + echo 'favicon exists' . PHP_EOL; + if ( stripos( $info['favicon'], 'http' ) !== false) { + echo 'favicon is full url' . PHP_EOL; + $imageUrl = $info['favicon']; + } else { + echo 'favicon is not full url' . PHP_EOL; + $imageUrl = trim( $base_url, '/' ) . '/' . ltrim( $info['favicon'], '/' ); + } + if ( $this->isValidImageUrl( $imageUrl ) ) { + echo 'image is valid' . PHP_EOL; + $info['favicon'] = $imageUrl; + } else { + echo 'image is not valid' . PHP_EOL; + $base_info = $this->getMetaTagsAndFavicon( $base_url ); + if ( ! empty( $base_info['favicon'] ) ) { + echo 'parent favicon exists!'; + if ( stripos( $base_info['favicon'], 'http' ) !== false) { + echo 'parent favicon is full url' . PHP_EOL; + $imageUrl = $base_info['favicon']; + } else { + echo 'parent favicon is not full url' . PHP_EOL; + $imageUrl = trim( $base_url, '/' ) . '/' . ltrim( $base_info['favicon'], '/' ); + } + if ( $this->isValidImageUrl( $imageUrl ) ) { + echo 'parent favicon image is valid' . PHP_EOL; + $info['favicon'] = $imageUrl; + } else { + echo 'parent favicon image is not valid' . PHP_EOL; + } + } + } + } else { + echo 'favicon does not exist' . PHP_EOL; + $base_info = $this->getMetaTagsAndFavicon( $base_url ); + if ( ! empty( $base_info['favicon'] ) ) { + echo 'parent favicon exists!' . PHP_EOL; + if ( stripos( $base_info['favicon'], 'http' ) !== false) { + echo 'parent favicon is full url' . PHP_EOL; + $imageUrl = $base_info['favicon']; + } else { + echo 'parent favicon is not full url' . PHP_EOL; + $imageUrl = trim( $base_url, '/' ) . '/' . ltrim( $base_info['favicon'], '/' ); + } + if ( $this->isValidImageUrl( $imageUrl ) ) { + echo 'parent favicon image is valid' . PHP_EOL; + $info['favicon'] = $imageUrl; + } else { + echo 'parent favicon image is not valid' . PHP_EOL; + } + } + } + + return $info; + } + + public function refreshInfo( $id ) { + if ( !Check::id( $id ) ) { + Debug::info( 'Bookmarks: illegal ID.' ); + return false; + } + $bookmark = self::findById( $id ); + if ( $bookmark == false ) { + Debug::info( 'Bookmarks not found.' ); + return false; + } + if ( $bookmark->createdBy != App::$activeUser->ID ) { + Debug::info( 'You do not have permission to modify this bookmark.' ); + return false; + } + if ( time() < ( $bookmark->refreshedAt + ( 60 * 10 ) ) ) { + Debug::info( 'You may only fetch bookmarks once every 10 minutes.' ); + return false; + } + + $info = $this->retrieveInfo( $bookmark->url ); + + $fields = [ + // 'refreshedAt' => time(), + ]; + + if ( empty( $bookmark->title ) && ! empty( $info['title'] ) ) { + $fields['title'] = $info['title']; + } + if ( empty( $bookmark->description ) && ! empty( $info['description'] ) ) { + $fields['description'] = $info['description']; + } + + if ( ( empty( $bookmark->icon ) || ! $this->isValidImageUrl( $bookmark->icon ) ) && ! empty( $info['favicon'] ) ) { + $fields['icon'] = $info['favicon']; + } + $fields['meta'] = json_encode( $info['metadata'] ); + + if ( !self::$db->update( $this->tableName, $id, $fields ) ) { + new CustomException( 'bookmarkUpdate' ); + Debug::error( "Bookmarks: $id not updated" ); + return false; + } + return true; + } + + private function getBaseUrl ( $url ) { + if ( empty($url) ) { + return $url; + } + + $parsedUrl = parse_url($url); + if (isset($parsedUrl['scheme']) && isset($parsedUrl['host'])) { + return $parsedUrl['scheme'] . '://' . $parsedUrl['host'] . '/'; + } else { + return null; // URL is not valid or cannot be parsed + } + } + + public function isValidImageUrl($url) { + $headers = @get_headers($url); + if ($headers && strpos($headers[0], '200') !== false) { + + return true; + // Further check to ensure it's an image + foreach ($headers as $header) { + if (strpos(strtolower($header), 'content-type:') !== false) { + if (strpos(strtolower($header), 'image/') !== false) { + return true; + } + } + } + } + return false; + } +} diff --git a/app/plugins/bookmarks/models/folders.php b/app/plugins/bookmarks/models/folders.php new file mode 100644 index 0000000..2655c36 --- /dev/null +++ b/app/plugins/bookmarks/models/folders.php @@ -0,0 +1,216 @@ + + * @link https://TheTempusProject.com + * @license https://opensource.org/licenses/MIT [MIT LICENSE] + */ +namespace TheTempusProject\Models; + +use TheTempusProject\Bedrock\Functions\Check; +use TheTempusProject\Canary\Bin\Canary as Debug; +use TheTempusProject\Classes\DatabaseModel; +use TheTempusProject\TheTempusProject as App; +use TheTempusProject\Houdini\Classes\Filters; +use TheTempusProject\Canary\Classes\CustomException; + +class Folders extends DatabaseModel { + public $tableName = 'folders'; + public $databaseMatrix = [ + [ 'title', 'varchar', '256' ], + [ 'color', 'varchar', '48' ], + [ 'privacy', 'varchar', '48' ], + [ 'description', 'text', '' ], + [ 'folderID', 'int', '11' ], + [ 'createdBy', 'int', '11' ], + [ 'createdAt', 'int', '11' ], + [ 'uuid', 'char', '36' ], + ]; + + /** + * The model constructor. + */ + public function __construct() { + parent::__construct(); + } + + public function findByUuid( $id ) { + $whereClause = ['uuid', '=', $id]; + if ( empty( $limit ) ) { + $folders = self::$db->get( $this->tableName, $whereClause ); + } else { + $folders = self::$db->get( $this->tableName, $whereClause, 'ID', 'DESC', [0, $limit] ); + } + if ( !$folders->count() ) { + Debug::info( 'No Folders found.' ); + return false; + } + return $this->filter( $folders->first() ); + } + + public function create( $title, $folderID = 0, $description = '', $color = 'default', $privacy = 'private', $user = null ) { + if ( empty( $user ) ) { + $user = App::$activeUser->ID; + } + if ( ! Check::dataTitle( $title ) ) { + Debug::info( 'Folders: illegal title.' ); + return false; + } + $fields = [ + 'title' => $title, + 'description' => $description, + 'color' => $color, + 'privacy' => $privacy, + 'uuid' => generateUuidV4(), + 'createdBy' => $user, + 'createdAt' => time(), + ]; + + if ( !empty( $folderID ) ) { + $fields['folderID'] = $folderID; + } + if ( ! self::$db->insert( $this->tableName, $fields ) ) { + new CustomException( 'folderCreate' ); + Debug::error( "Folders: not created " . var_export($fields,true) ); + return false; + } + return self::$db->lastId(); + } + + public function update( $id, $title, $folderID = 0, $description = '', $color = 'default', $privacy = 'private' ) { + if ( !Check::id( $id ) ) { + Debug::info( 'Folders: illegal ID.' ); + return false; + } + if ( !Check::dataTitle( $title ) ) { + Debug::info( 'Folders: illegal title.' ); + return false; + } + $fields = [ + 'title' => $title, + 'description' => $description, + 'color' => $color, + 'privacy' => $privacy, + ]; + if ( !empty( $folderID ) ) { + $fields['folderID'] = $folderID; + } + if ( !self::$db->update( $this->tableName, $id, $fields ) ) { + new CustomException( 'folderUpdate' ); + Debug::error( "Folders: $id not updated: $fields" ); + return false; + } + return true; + } + + public function byUser( $limit = null ) { + $whereClause = ['createdBy', '=', App::$activeUser->ID]; + if ( empty( $limit ) ) { + $folders = self::$db->get( $this->tableName, $whereClause ); + } else { + $folders = self::$db->get( $this->tableName, $whereClause, 'ID', 'DESC', [0, $limit] ); + } + if ( !$folders->count() ) { + Debug::info( 'No Folders found.' ); + return false; + } + return $this->filter( $folders->results() ); + } + + public function bySpecificUser( $userID, $limit = null ) { + $whereClause = ['createdBy', '=', $userID]; + if ( empty( $limit ) ) { + $folders = self::$db->get( $this->tableName, $whereClause ); + } else { + $folders = self::$db->get( $this->tableName, $whereClause, 'ID', 'DESC', [0, $limit] ); + } + if ( !$folders->count() ) { + Debug::info( 'No Folders found.' ); + return false; + } + return $this->filter( $folders->results() ); + } + + public function getName( $id ) { + $folder = self::findById( $id ); + if (false == $folder) { + return 'unknown'; + } + return $folder->title; + } + + public function getColor( $id ) { + $folder = self::findById( $id ); + if (false == $folder) { + return 'default'; + } + return $folder->color; + } + + public function simpleByUser() { + $whereClause = ['createdBy', '=', App::$activeUser->ID]; + $folders = self::$db->get( $this->tableName, $whereClause ); + if ( !$folders->count() ) { + Debug::warn( 'Could not find any folders' ); + return false; + } + + $folders = $folders->results(); + $out = []; + $out[ 'No Folder' ] = 0; + foreach ( $folders as $folder ) { + $out[ $folder->title ] = $folder->ID; + } + return $out; + } + + public function simpleObjectByUser() { + $whereClause = ['createdBy', '=', App::$activeUser->ID]; + $folders = self::$db->get( $this->tableName, $whereClause ); + if ( !$folders->count() ) { + Debug::warn( 'Could not find any folders' ); + return false; + } + + $folders = $folders->results(); + $out = []; + foreach ( $folders as $folder ) { + $obj = new \stdClass(); + $obj->title = $folder->title; + $obj->ID = $folder->ID; + $out[] = $obj; + } + return $out; + } + + public function filter( $data, $params = [] ) { + foreach ( $data as $instance ) { + if ( !is_object( $instance ) ) { + $instance = $data; + $end = true; + } + // Real Work Starts Here + $instance->prettyPrivacy = ucfirst( $instance->privacy ); + $instance->prettyTitle = ucfirst( $instance->title ); + + if ( $instance->privacy == 'private' ) { + $instance->privacyBadge = 'Private'; + } else { + $instance->privacyBadge = 'Public'; + } + + // Real Work Ends Here + $out[] = $instance; + if ( !empty( $end ) ) { + $out = $out[0]; + break; + } + } + return $out; + } +} diff --git a/app/plugins/bookmarks/plugin.php b/app/plugins/bookmarks/plugin.php new file mode 100644 index 0000000..a8b9aa2 --- /dev/null +++ b/app/plugins/bookmarks/plugin.php @@ -0,0 +1,180 @@ + + * @link https://TheTempusProject.com + * @license https://opensource.org/licenses/MIT [MIT LICENSE] + */ +namespace TheTempusProject\Plugins; + +use TheTempusProject\Classes\Plugin; +use TheTempusProject\Models\Events; +use TheTempusProject\Models\Bookmarks as Bookmark; +use TheTempusProject\Models\Folders; +use TheTempusProject\Houdini\Classes\Components; +use TheTempusProject\Houdini\Classes\Template; +use TheTempusProject\TheTempusProject as App; + +class Bookmarks extends Plugin { + public $pluginName = 'TP Bookmarks'; + public $configName = 'bookmarks'; + public $pluginAuthor = 'JoeyK'; + public $pluginWebsite = 'https://TheTempusProject.com'; + public $modelVersion = '1.0'; + public $pluginVersion = '3.0'; + public $pluginDescription = 'A simple plugin which adds a site wide bookmark system.'; + public $permissionMatrix = [ + 'useBookmarks' => [ + 'pretty' => 'Can use the bookmarks feature', + 'default' => false, + ], + 'createEvents' => [ + 'pretty' => 'Can add events to bookmarks', + 'default' => false, + ], + ]; + public $main_links = [ + [ + 'text' => 'Bookmarks', + 'url' => '{ROOT_URL}bookmarks/index', + 'filter' => 'loggedin', + ], + [ + 'text' => 'Tutorials', + 'url' => '{ROOT_URL}tutorials', + 'filter' => 'bkmtuts', + ], + [ + 'text' => 'Extensions', + 'url' => '{ROOT_URL}extensions/index', + ], + ]; + public $info_footer_links = [ + [ + 'text' => 'Tutorials', + 'url' => '{ROOT_URL}tutorials', + ], + ]; + public $configMatrix = [ + 'enabled' => [ + 'type' => 'radio', + 'pretty' => 'Enable Bookmarks.', + 'default' => true, + ], + ]; + public $preferenceMatrix = [ + 'editModeSwitch' => [ + 'pretty' => 'Bookmarks default setting for edit mode', + 'type' => 'checkbox', + 'default' => 'false', + ], + 'showArchivedSwitch' => [ + 'pretty' => 'Show archived bookmarks by default', + 'type' => 'checkbox', + 'default' => 'false', + ], + 'showHiddenSwitch' => [ + 'pretty' => 'Show hidden bookmarks by default', + 'type' => 'checkbox', + 'default' => 'false', + ], + 'archiveButtonSwitch' => [ + 'pretty' => 'Show the archive buttons by default', + 'type' => 'checkbox', + 'default' => 'false', + ], + 'visibilityButtonSwitch' => [ + 'pretty' => 'Show the visibility buttons by default', + 'type' => 'checkbox', + 'default' => 'false', + ], + 'privacyButtonSwitch' => [ + 'pretty' => 'Show the privacy buttons by default', + 'type' => 'checkbox', + 'default' => 'true', + ], + 'shareButtonSwitch' => [ + 'pretty' => 'Show the share buttons by default', + 'type' => 'checkbox', + 'default' => 'true', + ], + 'addButtonSwitch' => [ + 'pretty' => 'Show the add buttons by default', + 'type' => 'checkbox', + 'default' => 'true', + ], + 'bookmarkTutorials' => [ + 'pretty' => 'Show tutorials for the bookmark feature', + 'type' => 'checkbox', + 'default' => 'true', + ], + ]; + public $resourceMatrix = [ + 'routes' => [ + [ + 'original_url' => 'chrome', + 'redirect_type' => 'external', + 'nickname' => 'Chrome Extension', + 'forwarded_url' => 'https://chromewebstore.google.com/detail/allthebookmarks/hcofhopnjoodmakhhmgmoohgpdhfkgii?authuser=0&hl=en', + ], + [ + 'original_url' => 'brave', + 'redirect_type' => 'external', + 'nickname' => 'Brave Extension', + 'forwarded_url' => 'https://chromewebstore.google.com/detail/allthebookmarks/hcofhopnjoodmakhhmgmoohgpdhfkgii?authuser=0&hl=en', + ], + [ + 'original_url' => 'firefox', + 'redirect_type' => 'external', + 'nickname' => 'Firefox Extension', + 'forwarded_url' => 'https://addons.mozilla.org/en-US/firefox/addon/allthebookmarks/', + ], + // [ + // 'original_url' => 'edge', + // 'redirect_type' => 'external', + // 'nickname' => 'Edge Extension', + // 'forwarded_url' => 'https://www.facebook.com/thetempusproject', + // ], + // [ + // 'original_url' => 'opera', + // 'redirect_type' => 'external', + // 'nickname' => 'Opera Extension', + // 'forwarded_url' => 'https://www.facebook.com/thetempusproject', + // ], + ] + ]; + + public $bookmarks; + public $folders; + protected static $loaded = false; + + public function __construct( $load = false ) { + $this->bookmarks = new Bookmark; + $this->folders = new Folders; + + if ( ! self::$loaded && $load ) { + $tutpref = true; + if ( App::$isLoggedIn ) { + if ( empty( App::$activeUser->prefs['bookmarkTutorials'] ) ) { + $tutpref = false; + } + } + $this->filters[] = [ + 'name' => 'bkmtuts', + 'find' => '#{BKMTUTS}(.*?){/BKMTUTS}#is', + 'replace' => ( $tutpref ? '$1' : '' ), + 'enabled' => true, + ]; + } + + parent::__construct( $load ); + if ( ! self::$loaded && $load ) { + self::$loaded = true; + } + } +} diff --git a/app/plugins/bookmarks/views/bookmarks/create.html b/app/plugins/bookmarks/views/bookmarks/create.html new file mode 100644 index 0000000..59d4c98 --- /dev/null +++ b/app/plugins/bookmarks/views/bookmarks/create.html @@ -0,0 +1,56 @@ +
    +
    + Add Bookmark +
    +
    +
    +
    + +
    + +
    +
    + + +
    + +
    + +
    +
    + + +
    + +
    + +
    +
    + {folderSelect} +
    + +
    + +
    +
    +
    + +
    + {colorSelect} +
    +
    + + + + + +
    + +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/app/plugins/bookmarks/views/bookmarks/edit.html b/app/plugins/bookmarks/views/bookmarks/edit.html new file mode 100644 index 0000000..98db339 --- /dev/null +++ b/app/plugins/bookmarks/views/bookmarks/edit.html @@ -0,0 +1,56 @@ +
    +
    + Edit Bookmark +
    +
    +
    +
    + +
    + +
    +
    + + +
    + +
    + +
    +
    + + +
    + +
    + +
    +
    + {folderSelect} +
    + +
    + +
    +
    +
    + +
    + {colorSelect} +
    +
    + + + + + +
    + +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/app/plugins/bookmarks/views/bookmarks/list.html b/app/plugins/bookmarks/views/bookmarks/list.html new file mode 100644 index 0000000..fa55915 --- /dev/null +++ b/app/plugins/bookmarks/views/bookmarks/list.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + {LOOP} + + + + + + + + {/LOOP} + {ALT} + + + + {/ALT} + +
    Bookmark
    + + {title} + + + {privacy} +
    + No results to show. +
    +
    + Create +
    \ No newline at end of file diff --git a/app/plugins/bookmarks/views/bookmarks/listPage.html b/app/plugins/bookmarks/views/bookmarks/listPage.html new file mode 100644 index 0000000..45e81f0 --- /dev/null +++ b/app/plugins/bookmarks/views/bookmarks/listPage.html @@ -0,0 +1,13 @@ +
    + {LOOP} + Bookmark List +
    + {panel} +
    + {/LOOP} + {ALT} +
    +

    no folders

    +
    + {/ALT} +
    \ No newline at end of file diff --git a/app/plugins/bookmarks/views/bookmarks/unsorted.html b/app/plugins/bookmarks/views/bookmarks/unsorted.html new file mode 100644 index 0000000..952ce0b --- /dev/null +++ b/app/plugins/bookmarks/views/bookmarks/unsorted.html @@ -0,0 +1,54 @@ +
    +
    + Unsorted Bookmarks +
    + {BKMTUTS} +

    + When you add bookmarks without a folder, they will be considered "Unsorted". There is nothing wrong with these bookmarks, + but much of the application relies on folder for organizing and displaying bookmarks. You may find that these bookmarks do not regularly appear in other sections of the application. +

    +

    + This explanation and others like it can be hidden in your Profile Settings. +

    +
    + {/BKMTUTS} + + + + + + + + + + + + {LOOP} + + + + + + + + {/LOOP} + {ALT} + + + + {/ALT} + +
    Bookmark
    + + {title} + + + {privacy} +
    + No results to show. +
    +
    + Add +
    +
    +
    \ No newline at end of file diff --git a/app/plugins/bookmarks/views/bookmarks/view.html b/app/plugins/bookmarks/views/bookmarks/view.html new file mode 100644 index 0000000..b0f17b0 --- /dev/null +++ b/app/plugins/bookmarks/views/bookmarks/view.html @@ -0,0 +1,96 @@ +
    +
    +
    +
    + +
    +

    Bookmark

    +
    + + +
    +
    + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Title:{title}
    URL:{url}
    Type:{linkType}
    Privacy:{privacy}
    Color:{color}
    Created:{DTC}{createdAt}{/DTC}
    Archived:{DTC}{archivedAt}{/DTC}
    Hidden:{DTC}{hiddenAt}{/DTC}
    Last Refreshed:{DTC}{refreshedAt}{/DTC}
    Description
    {description}
    Icon
    {iconHtml}
    {icon}
    Meta
    {meta}
    +
    +
    +
    + + + +
    +
    +
    +
    \ No newline at end of file diff --git a/app/plugins/bookmarks/views/components/bookmarkListPanel.html b/app/plugins/bookmarks/views/components/bookmarkListPanel.html new file mode 100644 index 0000000..715727f --- /dev/null +++ b/app/plugins/bookmarks/views/components/bookmarkListPanel.html @@ -0,0 +1,56 @@ +{LOOP} +
    +
    +
    +
    + + + + + {title} + + + +
    + + + +
    +
    +
      + {bookmarkListRows} +
    +
    + +
    +
    +
    +
    +{/LOOP} +{ALT} +
    +

    No folders found.

    +
    +{/ALT} \ No newline at end of file diff --git a/app/plugins/bookmarks/views/components/bookmarkListRows.html b/app/plugins/bookmarks/views/components/bookmarkListRows.html new file mode 100644 index 0000000..9060780 --- /dev/null +++ b/app/plugins/bookmarks/views/components/bookmarkListRows.html @@ -0,0 +1,35 @@ +{LOOP} +
  7. + {iconHtml} + {title} + + {hideBtn} + {archiveBtn} + {publish} + + + + + + + +
  8. +{/LOOP} +{ALT} +
  9. +

    No Bookmarks

    +
  10. +{/ALT} \ No newline at end of file diff --git a/app/plugins/bookmarks/views/components/linkSelect.html b/app/plugins/bookmarks/views/components/linkSelect.html new file mode 100644 index 0000000..16b3a32 --- /dev/null +++ b/app/plugins/bookmarks/views/components/linkSelect.html @@ -0,0 +1,16 @@ +
    + + {LOOP} +
    + +

    +
    + + + {title} + +
    +

    +
    + {/LOOP} +
    \ No newline at end of file diff --git a/app/plugins/bookmarks/views/components/publicList.html b/app/plugins/bookmarks/views/components/publicList.html new file mode 100644 index 0000000..4dc8c85 --- /dev/null +++ b/app/plugins/bookmarks/views/components/publicList.html @@ -0,0 +1,27 @@ +{LOOP} +
    +
    +
    + +
    +
    +
    {title}
    +
    +
    + +
    +
    +
      + {bookmarkListRows} +
    +
    +
    +
    +
    +
    +{/LOOP} +{ALT} +
    +

    no Folders

    +
    +{/ALT} \ No newline at end of file diff --git a/app/plugins/bookmarks/views/components/publicListRows.html b/app/plugins/bookmarks/views/components/publicListRows.html new file mode 100644 index 0000000..85005ff --- /dev/null +++ b/app/plugins/bookmarks/views/components/publicListRows.html @@ -0,0 +1,30 @@ +{LOOP} +
  11. + {iconHtml} + {title} + + + + + + +
  12. +{/LOOP} +{ALT} +
  13. +

    No Bookmarks

    +
  14. +{/ALT} \ No newline at end of file diff --git a/app/plugins/bookmarks/views/components/shareListPanel.html b/app/plugins/bookmarks/views/components/shareListPanel.html new file mode 100644 index 0000000..4794f6c --- /dev/null +++ b/app/plugins/bookmarks/views/components/shareListPanel.html @@ -0,0 +1,54 @@ +{LOOP} +
    +
    +
    +
    +
    + + {privacyBadge} + {title} + + + + +
    +
    + + + +
    +
    +
      + {bookmarkListRows} +
    +
    + +
    +
    +
    +
    +{/LOOP} +{ALT} +
    +

    no Folders

    +
    +{/ALT} \ No newline at end of file diff --git a/app/plugins/bookmarks/views/components/shareListRows.html b/app/plugins/bookmarks/views/components/shareListRows.html new file mode 100644 index 0000000..06d825d --- /dev/null +++ b/app/plugins/bookmarks/views/components/shareListRows.html @@ -0,0 +1,31 @@ +{LOOP} +
  15. + {iconHtml} + {title}{privacyBadge} + + {publish} + + + + + +
  16. +{/LOOP} +{ALT} +
  17. +

    No Bookmarks

    +
  18. +{/ALT} \ No newline at end of file diff --git a/app/plugins/bookmarks/views/dash.html b/app/plugins/bookmarks/views/dash.html new file mode 100644 index 0000000..308a7ee --- /dev/null +++ b/app/plugins/bookmarks/views/dash.html @@ -0,0 +1,25 @@ +
    +
    + Manage Bookmarks +
    + {BKMTUTS} +

    + Dashboards are one of our most powerful and useful features of AllTheBookmarks. With dashboards, you're able to create, edit, share, and use pages built dynamically from your bookmark folders. + When you create a dashboard, you select any number of your organized folders, select a few display options, and you gain access to a new page just for those folders. +

    +

    + Sometimes, there just isn't enough room on the toolbar. Instead of having 6 folders you always use, have 1, short link called "Games" that opens 1 page. I use this to group small sections of + my bookmarks together like games. I have PalWorld, WoW, Satisfactory, and Diablo links in a "Games" dashboard. I can just quickly go to that dash, find the folder I need and go. +

    +

    + This explanation and others like it can be hidden in your Profile Settings. +

    +
    + {/BKMTUTS} + {VIEW_OPTIONS} +
    +
    + {folderPanels} +
    +
    +
    \ No newline at end of file diff --git a/app/plugins/bookmarks/views/dashboardExplainer.html b/app/plugins/bookmarks/views/dashboardExplainer.html new file mode 100644 index 0000000..daf1592 --- /dev/null +++ b/app/plugins/bookmarks/views/dashboardExplainer.html @@ -0,0 +1,14 @@ +
    +
    + Bookmark Dashboards +
    +

    + Dashboards are one of our most powerful and useful features of AllTheBookmarks. With dashboards, you're able to create, edit, share, and use pages built dynamically from your bookmark folders. + When you create a dashboard, you select any number of your organized folders, select a few display options, and you gain access to a new page just for those folders. +

    +

    + Sometimes, there just isn't enough room on the toolbar. Instead of having 6 folders you always use, have 1, short link called "Games" that opens 1 page. I use this to group small sections of + my bookmarks together like games. I have PalWorld, WoW, Satisfactory, and Diablo links in a "Games" dashboard. I can just quickly go to that dash, find the folder I need and go. +

    +
    +
    \ No newline at end of file diff --git a/app/plugins/bookmarks/views/dashboards/bookmarkRows.html b/app/plugins/bookmarks/views/dashboards/bookmarkRows.html new file mode 100644 index 0000000..5fe5243 --- /dev/null +++ b/app/plugins/bookmarks/views/dashboards/bookmarkRows.html @@ -0,0 +1,30 @@ +{LOOP} +
  19. + {iconHtml} + {title} + + + + + + +
  20. +{/LOOP} +{ALT} +
  21. +

    No Bookmarks

    +
  22. +{/ALT} \ No newline at end of file diff --git a/app/plugins/bookmarks/views/dashboards/create.html b/app/plugins/bookmarks/views/dashboards/create.html new file mode 100644 index 0000000..8018a51 --- /dev/null +++ b/app/plugins/bookmarks/views/dashboards/create.html @@ -0,0 +1,43 @@ +
    +
    + Add Dashboard +
    +

    Dashboards are groups of folders that usually share a theme. When links are added to folders they will automatically be added to dashboards unless hidden or archived.

    +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + {DASH_OPTIONS} +
    +
    +
    + +
    + {LINK_SELECT} +
    +
    + + + + + +
    + +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/app/plugins/bookmarks/views/dashboards/dashOptions.html b/app/plugins/bookmarks/views/dashboards/dashOptions.html new file mode 100644 index 0000000..609473d --- /dev/null +++ b/app/plugins/bookmarks/views/dashboards/dashOptions.html @@ -0,0 +1,27 @@ +
    + +
    +
    Status:
    +
    + + +
    +
    + + +
    +
    + + +
    +
    Buttons:
    +
    + + +
    +
    + + +
    +
    +
    \ No newline at end of file diff --git a/app/plugins/bookmarks/views/dashboards/edit.html b/app/plugins/bookmarks/views/dashboards/edit.html new file mode 100644 index 0000000..ee6b7c6 --- /dev/null +++ b/app/plugins/bookmarks/views/dashboards/edit.html @@ -0,0 +1,43 @@ +
    +
    + Edit Dashboard +
    +

    Dashboards are groups of folders that usually share a theme. When links are added to folders they will automatically be added to dashboards unless hidden or archived.

    +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + {DASH_OPTIONS} +
    +
    +
    + +
    + {LINK_SELECT} +
    +
    + + + + + +
    + +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/app/plugins/bookmarks/views/dashboards/folderPanels.html b/app/plugins/bookmarks/views/dashboards/folderPanels.html new file mode 100644 index 0000000..101df2c --- /dev/null +++ b/app/plugins/bookmarks/views/dashboards/folderPanels.html @@ -0,0 +1,54 @@ +{LOOP} +
    +
    +
    +
    + + + + + {title} + + + +
    + + + +
    +
    +
      + {bookmarkRows} +
    +
    + +
    +
    +
    +
    +{/LOOP} +{ALT} +
    +

    No folders found.

    +
    +{/ALT} \ No newline at end of file diff --git a/app/plugins/bookmarks/views/dashboards/list.html b/app/plugins/bookmarks/views/dashboards/list.html new file mode 100644 index 0000000..9d8b3e3 --- /dev/null +++ b/app/plugins/bookmarks/views/dashboards/list.html @@ -0,0 +1,54 @@ +
    +
    + Dashboards +
    + {BKMTUTS} +

    + Dashboards are one of our most powerful and useful features of AllTheBookmarks. With dashboards, you're able to create, edit, share, and use pages built dynamically from your bookmark folders. + When you create a dashboard, you select any number of your organized folders, select a few display options, and you gain access to a new page just for those folders. +

    +

    + Sometimes, there just isn't enough room on the toolbar. Instead of having 6 folders you always use, have 1, short link called "Games" that opens 1 page. I use this to group small sections of + my bookmarks together like games. I have PalWorld, WoW, Satisfactory, and Diablo links in a "Games" dashboard. I can just quickly go to that dash, find the folder I need and go. +

    +

    + This explanation and others like it can be hidden in your Profile Settings. +

    +
    + {/BKMTUTS} +
    + + + + + + + + + + + + {LOOP} + + + + + + + + {/LOOP} + {ALT} + + + + {/ALT} + +
    TitleDescription
    {title}{description}
    + No results to show. +
    +
    + Create +
    +
    +
    +
    \ No newline at end of file diff --git a/app/plugins/bookmarks/views/dashboards/view.html b/app/plugins/bookmarks/views/dashboards/view.html new file mode 100644 index 0000000..fc574d1 --- /dev/null +++ b/app/plugins/bookmarks/views/dashboards/view.html @@ -0,0 +1,29 @@ +
    +
    + {title} +
    +
    +
    + + + +
    + {folderPanels} +
    +
    +
    +
    +
    + {DASH_OPTIONS} +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/app/plugins/bookmarks/views/export.html b/app/plugins/bookmarks/views/export.html new file mode 100644 index 0000000..fb9f18f --- /dev/null +++ b/app/plugins/bookmarks/views/export.html @@ -0,0 +1,84 @@ +
    +
    + Bookmark Export +
    + {BKMTUTS} +

    + Our export tool allows you to select bookmarks and folders from your AllTheBookmarks account and generate a file that you can then import into any browser. + Giving you access to all your bookmarks right inside your browser. +

    +

    + You can simply select the folders you want to bring into your browser on this page and click generate. A notification will pop-up showing you where you can + find your file for download. You take that newly saved file an import it into your browser of choice. +

    +

    + If you need help importing your bookmarks, we have tutorials available for every major browser that can help. +

    +

    + This explanation and others like it can be hidden in your Profile Settings. +

    +
    + {/BKMTUTS} +

    Select which folders to include in the export

    +
    +
    + + + + + + + + + + + + + {LOOP} + + + + + {/LOOP} + {ALT} + + + + {/ALT} + +
    Title + +
    Unsorted + +
    {prettyTitle} + +
    + No Folders To Export +
    +
    +
    + +
    +
    + +
    +
    Status:
    +
    + + +
    +
    + + +
    +
    +
    +
    +
    +
    +

    Literally every browser is chromium based now, so they all have a standard import/export.

    + +
    +
    +
    +
    \ No newline at end of file diff --git a/app/plugins/bookmarks/views/exportExplainer.html b/app/plugins/bookmarks/views/exportExplainer.html new file mode 100644 index 0000000..fc332ba --- /dev/null +++ b/app/plugins/bookmarks/views/exportExplainer.html @@ -0,0 +1,17 @@ +
    +
    + Bookmark Export +
    +

    + Our export tool allows you to select bookmarks and folders from your AllTheBookmarks account and generate a file that you can then import into any browser. + Giving you access to all your bookmarks right inside your browser. +

    +

    + You can simply select the folders you want to bring into your browser on this page and click generate. A notification will pop-up showing you where you can + find your file for download. You take that newly saved file an import it into your browser of choice. +

    +

    + If you need help importing your bookmarks, we have tutorials available for every major browser that can help. +

    +
    +
    \ No newline at end of file diff --git a/app/plugins/bookmarks/views/extensions/bookmarklet.html b/app/plugins/bookmarks/views/extensions/bookmarklet.html new file mode 100644 index 0000000..fff29c9 --- /dev/null +++ b/app/plugins/bookmarks/views/extensions/bookmarklet.html @@ -0,0 +1,108 @@ +
    + Mobile Bookmarklet +
    +
    +
    +

    + You can quickly and easily add bookmarks to your AllTheBookmarks account from any mobile device. + Since you can't use extensions on mobile, you can log in from your mobile device and visit this page. +

    +

    + Below is a code snippet that you can copy. Copy the code, then create a new bookmark on your mobile device as you would normally. + Before saving the bookmark, change the name to something simple like "ATB Bookmarker" and paste this code as the url. +

    +

    + Once you have the bookmarklet saved, you can simply go to your mobile bookmarks while on a page and this snippet will grab the info you need and add it to you account. +

    +
    +
    +
    +
    + +
    +
    Default privacy:
    +
    + +
    +
    + +
    +
    Ask for Description:
    +
    + + +
    +
    + +
    +
    Include a default color:
    +
    + + +
    +
    + {colorSelect} +
    +
    + +
    +
    Include a default folder:
    +
    + + +
    +
    + {folderSelect} +
    +
    +
    + +
    +
    + +
    +
    +                javascript:(function() {
    +                    const apiKey = "{BK_API_KEY}";
    +                    const apiUrl = "{BK_API_URL}";
    +                    const url = window.location.href;
    +                    const name = prompt("Enter a name for the bookmark:");
    +                    {BK_JS_NOTES}
    +                    {BK_JS_COLOR}
    +                    {BK_JS_PRIVACY}
    +                    {BK_JS_FOLDER}
    +
    +                    if (!name) {
    +                        alert("Name is required.");
    +                        return;
    +                    }
    +                
    +                    fetch(apiUrl + 'api/bookmarks/create', {
    +                        method: 'POST',
    +                        headers: {
    +                            'Content-Type': 'application/json',
    +                            'Authorization': `Bearer ${apiKey}`
    +                        },
    +                        body: JSON.stringify({ name, url, notes, color, privacy, folder })
    +                    })
    +                    .then(response => {
    +                        if (response.ok) {
    +                            alert("Bookmark saved successfully!");
    +                        } else {
    +                            alert("Failed to save bookmark. Please check your API key.");
    +                        }
    +                    })
    +                    .catch(error => {
    +                        console.error(error);
    +                        alert("An unknown error occurred while saving the bookmark.");
    +                    });
    +                })();
    +            
    +
    +
    +
    \ No newline at end of file diff --git a/app/plugins/bookmarks/views/extensions/bookmarkletExplainer.html b/app/plugins/bookmarks/views/extensions/bookmarkletExplainer.html new file mode 100644 index 0000000..a90774a --- /dev/null +++ b/app/plugins/bookmarks/views/extensions/bookmarkletExplainer.html @@ -0,0 +1,19 @@ +
    + Mobile Bookmarklet +
    +
    +
    +

    + You can quickly and easily add bookmarks to your AllTheBookmarks account from any mobile device. + Since you can't use extensions on mobile, you can log in from your mobile device and visit this page. +

    +

    + Once logged in, there will be a code snippet that you can copy. Copy the code, then create a new bookmark on your mobile device as you would normally. + Before saving the bookmark, change the name to something simple like "ATB Bookmarker" and paste this code as the url. +

    +

    + Once you have the bookmarklet saved, you can simply go to your mobile bookmarks while on a page and this snippet will grab the info you need and add it to you account. +

    +
    +
    +
    \ No newline at end of file diff --git a/app/plugins/bookmarks/views/extensions/brave.html b/app/plugins/bookmarks/views/extensions/brave.html new file mode 100644 index 0000000..18347c3 --- /dev/null +++ b/app/plugins/bookmarks/views/extensions/brave.html @@ -0,0 +1,62 @@ +
    + +
    + +
    Brave Extension
    +

    Your ultimate Brave addon to organize, store, and personalize your bookmarks effortlessly.

    +
    + + +
    + +
    +
    +
    + +
    Add & Manage Bookmarks
    +

    Quickly add bookmarks, organize them, and never lose track of your favorite websites.

    +
    +
    +
    + + +
    +
    +
    + +
    Create Folders
    +

    Group your bookmarks into customizable folders for better organization.

    +
    +
    +
    + + +
    +
    +
    + +
    Set Colors
    +

    Personalize your folders and bookmarks with color coding.

    +
    +
    +
    + + +
    +
    +
    + +
    Privacy Controls
    +

    Keep your bookmarks private or share them—your choice, your control.

    +
    +
    +
    +
    + + + +
    diff --git a/app/plugins/bookmarks/views/extensions/chrome.html b/app/plugins/bookmarks/views/extensions/chrome.html new file mode 100644 index 0000000..0290a41 --- /dev/null +++ b/app/plugins/bookmarks/views/extensions/chrome.html @@ -0,0 +1,62 @@ +
    + +
    + +
    Chrome Extension
    +

    Your ultimate Chrome addon to organize, store, and personalize your bookmarks effortlessly.

    +
    + + +
    + +
    +
    +
    + +
    Add & Manage Bookmarks
    +

    Quickly add bookmarks, organize them, and never lose track of your favorite websites.

    +
    +
    +
    + + +
    +
    +
    + +
    Create Folders
    +

    Group your bookmarks into customizable folders for better organization.

    +
    +
    +
    + + +
    +
    +
    + +
    Set Colors
    +

    Personalize your folders and bookmarks with color coding.

    +
    +
    +
    + + +
    +
    +
    + +
    Privacy Controls
    +

    Keep your bookmarks private or share them—your choice, your control.

    +
    +
    +
    +
    + + + +
    diff --git a/app/plugins/bookmarks/views/extensions/edge.html b/app/plugins/bookmarks/views/extensions/edge.html new file mode 100644 index 0000000..29e9f70 --- /dev/null +++ b/app/plugins/bookmarks/views/extensions/edge.html @@ -0,0 +1,62 @@ +
    + +
    + +
    Edge Extension
    +

    Your ultimate Edge addon to organize, store, and personalize your bookmarks effortlessly.

    +
    + + +
    + +
    +
    +
    + +
    Add & Manage Bookmarks
    +

    Quickly add bookmarks, organize them, and never lose track of your favorite websites.

    +
    +
    +
    + + +
    +
    +
    + +
    Create Folders
    +

    Group your bookmarks into customizable folders for better organization.

    +
    +
    +
    + + +
    +
    +
    + +
    Set Colors
    +

    Personalize your folders and bookmarks with color coding.

    +
    +
    +
    + + +
    +
    +
    + +
    Privacy Controls
    +

    Keep your bookmarks private or share them—your choice, your control.

    +
    +
    +
    +
    + + + +
    diff --git a/app/plugins/bookmarks/views/extensions/firefox.html b/app/plugins/bookmarks/views/extensions/firefox.html new file mode 100644 index 0000000..df2e1cb --- /dev/null +++ b/app/plugins/bookmarks/views/extensions/firefox.html @@ -0,0 +1,62 @@ +
    + +
    + +
    Firefox Extension
    +

    Your ultimate Firefox addon to organize, store, and personalize your bookmarks effortlessly.

    +
    + + +
    + +
    +
    +
    + +
    Add & Manage Bookmarks
    +

    Quickly add bookmarks, organize them, and never lose track of your favorite websites.

    +
    +
    +
    + + +
    +
    +
    + +
    Create Folders
    +

    Group your bookmarks into customizable folders for better organization.

    +
    +
    +
    + + +
    +
    +
    + +
    Set Colors
    +

    Personalize your folders and bookmarks with color coding.

    +
    +
    +
    + + +
    +
    +
    + +
    Privacy Controls
    +

    Keep your bookmarks private or share them—your choice, your control.

    +
    +
    +
    +
    + + + +
    diff --git a/app/plugins/bookmarks/views/extensions/index.html b/app/plugins/bookmarks/views/extensions/index.html new file mode 100644 index 0000000..aba589e --- /dev/null +++ b/app/plugins/bookmarks/views/extensions/index.html @@ -0,0 +1,78 @@ +
    + +
    +

    Our Browser Extensions

    +

    Discover powerful tools to enhance your browsing experience, compatible with the browsers you love.

    +
    + + +
    + +
    +
    +
    + +
    Chrome Extension
    +

    Enhance your Chrome browser with advanced features and effortless organization.

    + Learn More +
    +
    +
    + + +
    +
    +
    + +
    Firefox Extension
    +

    Seamlessly integrate with Firefox for a smoother browsing experience.

    + Learn More +
    +
    +
    + + +
    +
    +
    + +
    Opera Extension
    +

    Boost your Opera browser with intuitive features and tools.

    + Learn More +
    +
    +
    + + +
    +
    +
    + +
    Brave Extension
    +

    Enjoy secure and private browsing with Brave, enhanced by our extension.

    + Learn More +
    +
    +
    + + +
    +
    +
    + +
    Edge Extension
    +

    Maximize productivity on Microsoft Edge with our extension.

    + Learn More +
    +
    +
    +
    + + +
    +

    + + Unfortunately, our extensions are not currently supported on Safari. +

    +
    +
    diff --git a/app/plugins/bookmarks/views/extensions/opera.html b/app/plugins/bookmarks/views/extensions/opera.html new file mode 100644 index 0000000..4d058ec --- /dev/null +++ b/app/plugins/bookmarks/views/extensions/opera.html @@ -0,0 +1,62 @@ +
    + +
    + +
    Opera Extension
    +

    Your ultimate Opera addon to organize, store, and personalize your bookmarks effortlessly.

    +
    + + +
    + +
    +
    +
    + +
    Add & Manage Bookmarks
    +

    Quickly add bookmarks, organize them, and never lose track of your favorite websites.

    +
    +
    +
    + + +
    +
    +
    + +
    Create Folders
    +

    Group your bookmarks into customizable folders for better organization.

    +
    +
    +
    + + +
    +
    +
    + +
    Set Colors
    +

    Personalize your folders and bookmarks with color coding.

    +
    +
    +
    + + +
    +
    +
    + +
    Privacy Controls
    +

    Keep your bookmarks private or share them—your choice, your control.

    +
    +
    +
    +
    + + + +
    diff --git a/app/plugins/bookmarks/views/extensions/safari.html b/app/plugins/bookmarks/views/extensions/safari.html new file mode 100644 index 0000000..b994aef --- /dev/null +++ b/app/plugins/bookmarks/views/extensions/safari.html @@ -0,0 +1,9 @@ +
    + +
    + +

    AllTheBookmarks

    +
    Safari Extension
    +

    I'm just gonna level with ya here, I have no idea if this will work on safari, and I'm not going to buy an apple product to find out. If you would like to gift me a relatively modern mac computer, I will happily test and find out.

    +
    +
    diff --git a/app/plugins/bookmarks/views/folders/create.html b/app/plugins/bookmarks/views/folders/create.html new file mode 100644 index 0000000..c785f56 --- /dev/null +++ b/app/plugins/bookmarks/views/folders/create.html @@ -0,0 +1,48 @@ + +
    +
    + Add Folder +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    + {folderSelect} +
    + +
    + +
    +
    +
    + +
    + {colorSelect} +
    +
    + + + + + +
    + +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/app/plugins/bookmarks/views/folders/edit.html b/app/plugins/bookmarks/views/folders/edit.html new file mode 100644 index 0000000..57671ad --- /dev/null +++ b/app/plugins/bookmarks/views/folders/edit.html @@ -0,0 +1,46 @@ +
    +
    + Edit Folder +
    +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    + {folderSelect} +
    + +
    + +
    +
    +
    + +
    + {colorSelect} +
    +
    + + + + + +
    + +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/app/plugins/bookmarks/views/folders/list.html b/app/plugins/bookmarks/views/folders/list.html new file mode 100644 index 0000000..019c76e --- /dev/null +++ b/app/plugins/bookmarks/views/folders/list.html @@ -0,0 +1,34 @@ + + + + + + + + + + + {LOOP} + + + + + + + {/LOOP} + {ALT} + + + + {/ALT} + +
    TitlePrivacyDescription
    {prettyTitle}{prettyPrivacy}{description} + + + +
    + No results to show. +
    +
    + Create +
    \ No newline at end of file diff --git a/app/plugins/bookmarks/views/folders/listPage.html b/app/plugins/bookmarks/views/folders/listPage.html new file mode 100644 index 0000000..cc5dd91 --- /dev/null +++ b/app/plugins/bookmarks/views/folders/listPage.html @@ -0,0 +1,14 @@ +
    + Folders List +
    + {BKMTUTS} +

    + Just like the folders you have in your web browser, you can add, edit, or remove folders for organizing your bookmarks here. +

    +

    + This explanation and others like it can be hidden in your Profile Settings. +

    +
    + {/BKMTUTS} + {foldersList} +
    \ No newline at end of file diff --git a/app/plugins/bookmarks/views/folders/view.html b/app/plugins/bookmarks/views/folders/view.html new file mode 100644 index 0000000..22124b9 --- /dev/null +++ b/app/plugins/bookmarks/views/folders/view.html @@ -0,0 +1,61 @@ +
    +
    +
    +
    + +
    +

    Bookmark Folder

    +
    + + +
    +
    + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + +
    Title:{title}
    Privacy:{privacy}
    Color:{color}
    Created:{DTC}{createdAt}{/DTC}
    Description
    {description}
    +
    +
    +
    + + + +
    +
    +
    +
    \ No newline at end of file diff --git a/app/plugins/bookmarks/views/import.html b/app/plugins/bookmarks/views/import.html new file mode 100644 index 0000000..789e883 --- /dev/null +++ b/app/plugins/bookmarks/views/import.html @@ -0,0 +1,31 @@ +
    +
    + Bookmark Import +
    + {BKMTUTS} +

    + Our import tool allows you to upload a file from you browser (Chrome, Firefox, etc) that will add all of your bookmarks and folders to your allthebookmarks.com dashboard. +

    +

    + The tool has been tested with all major browsers except safari. This includes Firefox, Chrome, Opera, Brave, and Edge. + You will need to browse your computer and select the .html file where you exported your bookmarks from your browser. +

    +

    + If you need help exporting your bookmarks, we have tutorials available for every major browser that can help. +

    +

    + This explanation and others like it can be hidden in your Profile Settings. +

    +
    + {/BKMTUTS} +
    +
    + + +
    +
    + +
    +
    +
    +
    \ No newline at end of file diff --git a/app/plugins/bookmarks/views/importExplainer.html b/app/plugins/bookmarks/views/importExplainer.html new file mode 100644 index 0000000..68c4160 --- /dev/null +++ b/app/plugins/bookmarks/views/importExplainer.html @@ -0,0 +1,16 @@ +
    +
    + Bookmark Import +
    +

    + Our import tool allows you to upload a file from you browser (Chrome, Firefox, etc) that will add all of your bookmarks and folders to your allthebookmarks.com dashboard. +

    +

    + The tool has been tested with all major browsers except safari. This includes Firefox, Chrome, Opera, Brave, and Edge. + You will need to browse your computer and select the .html file where you exported your bookmarks from your browser. +

    +

    + If you need help exporting your bookmarks, we have tutorials available for every major browser that can help. +

    +
    +
    \ No newline at end of file diff --git a/app/plugins/bookmarks/views/indexExplainer.html b/app/plugins/bookmarks/views/indexExplainer.html new file mode 100644 index 0000000..a30b67f --- /dev/null +++ b/app/plugins/bookmarks/views/indexExplainer.html @@ -0,0 +1,19 @@ +
    +
    + Manage Bookmarks +
    +

    + This is one of the main pages you'll be using to manage your bookmarks. Here you will find a list of all of your folders and the bookmarks they contain. + Hidden, archived, private, and public bookmarks can all be reviewed on this page using the filter options below. Some controls may be hidden by default, + so you're encouraged to toggle them to better understand your options for managing your bookmarks. +

    +

    + Once you start collecting hundreds and thousands of bookmarks, this page can get a bit overwhelming. We encourage you to check out our Dashboards feature + for a more robust way to interact with all your saved bookmarks. Make use of the ability to hide and archive bookmarks to keep organized as well. +

    + +
    +
    \ No newline at end of file diff --git a/app/plugins/bookmarks/views/introduction.html b/app/plugins/bookmarks/views/introduction.html new file mode 100644 index 0000000..09bfdaf --- /dev/null +++ b/app/plugins/bookmarks/views/introduction.html @@ -0,0 +1,58 @@ +
    + +
    +

    Welcome to All The Bookmarks

    +

    You've unlocked the ultimate tool for organizing, sharing, and managing all of your bookmarks effortlessly.

    + Illustration of bookmark organization +
    +
    + +
    +
    + +
    Organize Your Bookmarks
    +

    Create folders to keep your bookmarks tidy and easy to find.

    +
    +
    + +
    Share Collections
    +

    Share dashboards with groups of folders to collaborate seamlessly.

    +
    +
    + +
    Import & Export
    +

    Sync your bookmarks across major browsers with our easy import/export tools.

    +
    +
    +
    + +
    +
    +

    Bookmark Anything; Anytime, Anywhere

    +

    Our browser extension works with Chrome, Firefox, Brave, Edge, and Opera, making it simple to save and manage bookmarks directly from your browser.

    + Install Extension +
    +
    + Illustration of browser extensions +
    +
    +
    + +
    +
    + Illustration of dashboards +
    +
    +

    Dynamic Dashboards

    +

    Group folders into dashboards and share them to keep everyone updated on the latest changes.

    + Learn More About Dashboards +
    +
    +
    + +
    +

    Ready to Get Started?

    +

    Start organizing, sharing, and syncing your bookmarks today!

    + Create Your First Folder +
    +
    \ No newline at end of file diff --git a/app/plugins/bookmarks/views/nav/folderTabs.html b/app/plugins/bookmarks/views/nav/folderTabs.html new file mode 100644 index 0000000..6303c91 --- /dev/null +++ b/app/plugins/bookmarks/views/nav/folderTabs.html @@ -0,0 +1,10 @@ + +{userFolderTabs} \ No newline at end of file diff --git a/app/plugins/bookmarks/views/nav/tutorialCrumbs.html b/app/plugins/bookmarks/views/nav/tutorialCrumbs.html new file mode 100644 index 0000000..e69de29 diff --git a/app/plugins/bookmarks/views/nav/userFolderTabs.html b/app/plugins/bookmarks/views/nav/userFolderTabs.html new file mode 100644 index 0000000..7ac7ccc --- /dev/null +++ b/app/plugins/bookmarks/views/nav/userFolderTabs.html @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/app/plugins/bookmarks/views/nav/viewOptions.html b/app/plugins/bookmarks/views/nav/viewOptions.html new file mode 100644 index 0000000..de5e889 --- /dev/null +++ b/app/plugins/bookmarks/views/nav/viewOptions.html @@ -0,0 +1,66 @@ +
    +
    +
    + +
    +
    +
    +
    +
    + +
    +
    Edit Mode:
    +
    + + +
    +
    + + +
    +
    Status:
    +
    + + +
    +
    + + +
    +
    + + +
    +
    Buttons:
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    +
    + +
    +
    +
    +
    diff --git a/app/plugins/bookmarks/views/public/bookmark.html b/app/plugins/bookmarks/views/public/bookmark.html new file mode 100644 index 0000000..aa8e238 --- /dev/null +++ b/app/plugins/bookmarks/views/public/bookmark.html @@ -0,0 +1,48 @@ +
    +
    +
    +
    + +
    +

    Bookmark

    +
    + + +
    +
    + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + +
    Title:{title}
    URL:{url}
    Created:{DTC}{createdAt}{/DTC}
    Last Refreshed:{DTC}{refreshedAt}{/DTC}
    Icon
    {iconHtml}
    +
    +
    +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/app/plugins/bookmarks/views/public/folder.html b/app/plugins/bookmarks/views/public/folder.html new file mode 100644 index 0000000..8008fd2 --- /dev/null +++ b/app/plugins/bookmarks/views/public/folder.html @@ -0,0 +1,49 @@ +
    +
    +
    +
    + +
    +

    Bookmark Folder

    +
    + + +
    +
    + + +
    + + + + + + + + + + + + + + + + + + + + + +
    Title:{title}
    Color:{color}
    Added on:{DTC date}{createdAt}{/DTC}
    Description
    {description}
    +
    +
    +
    +
    +
    +
    +
    +
    + {panel} +
    +
    +
    \ No newline at end of file diff --git a/app/plugins/bookmarks/views/share.html b/app/plugins/bookmarks/views/share.html new file mode 100644 index 0000000..58dba19 --- /dev/null +++ b/app/plugins/bookmarks/views/share.html @@ -0,0 +1,30 @@ +
    +
    + Share +
    + {BKMTUTS} +
    +

    + This is the share page, designed to very quickly show you what folders and urls you have set to public and have the ability to share with anyone. +

    +

    + Any link or folder can be shared and by default, the extensions and app default to private. These "public" items can be viewed by anyone with the link provided from this page. +

    +

    + This explanation and others like it can be hidden in your Profile Settings. +

    +
    +
    + {/BKMTUTS} +
    + {LOOP} + {panel} + {/LOOP} + {ALT} +
    +

    No public folders found.

    +
    + {/ALT} +
    +
    +
    \ No newline at end of file diff --git a/app/plugins/bookmarks/views/shareExplainer.html b/app/plugins/bookmarks/views/shareExplainer.html new file mode 100644 index 0000000..b742d53 --- /dev/null +++ b/app/plugins/bookmarks/views/shareExplainer.html @@ -0,0 +1,12 @@ +
    +
    + Share +
    +

    + This is the share page, designed to very quickly show you what folders and urls you have set to public and have the ability to share with anyone. +

    +

    + Any link or folder can be shared and by default, the extensions and app default to private. These "public" items can be viewed by anyone with the link provided from this page. +

    +
    +
    \ No newline at end of file diff --git a/app/plugins/bookmarks/views/tutorials/brave/export.html b/app/plugins/bookmarks/views/tutorials/brave/export.html new file mode 100644 index 0000000..3536c8f --- /dev/null +++ b/app/plugins/bookmarks/views/tutorials/brave/export.html @@ -0,0 +1,43 @@ +
    +
    +
    +

    How to Export bookmarks from Brave

    + {tutorialCrumbs} +
    +

    + Exporting your bookmarks is lightning fast and simple inside of brave. +

    +
    Step 1: open the main application menu
    +

    + Simply left-click these three lines on the right side of your main toolbar. +

    +
    + Right-clicking on the extension icon +
    +
    Step 2: Open the Bookmark Manager
    +

    + In the list, hover over Bookmarks and Lists then select Bookmark Manager. +

    +
    + Extension settings page +
    +
    Step 2: Open the Export Menu
    +

    + On this page, in the top right, you should see another set of three dots, click these. +

    +
    + Extension settings page +
    +

    + From this list, select Export Bookmarks. +

    +
    + Extension settings page +
    + +

    + Finally, save the .html file somewhere easy to access when you use the imports feature. +

    +
    +
    +
    \ No newline at end of file diff --git a/app/plugins/bookmarks/views/tutorials/brave/import.html b/app/plugins/bookmarks/views/tutorials/brave/import.html new file mode 100644 index 0000000..9994cf6 --- /dev/null +++ b/app/plugins/bookmarks/views/tutorials/brave/import.html @@ -0,0 +1,45 @@ +
    +
    +
    +

    How to Import bookmarks in Brave

    + {tutorialCrumbs} +
    +

    + Importing your bookmarks from AllTheBookmarks is lightning fast and simple inside of brave. +

    +
    Step 1: open the main application menu
    +

    + Simply left-click these three lines on the right side of your main toolbar. +

    +
    + Right-clicking on the extension icon +
    +
    Step 2: Open the Bookmark Manager
    +

    + In the list, hover over Bookmarks and Lists then select Bookmark Manager. +

    +
    + Extension settings page +
    +
    Step 2: Open the Import Menu
    + +

    + On this page, in the top right, you should see another set of three dots, click these. +

    +
    + Extension settings page +
    + +

    + From this list, select Import Bookmarks. +

    +
    + Extension settings page +
    + +

    + Finally, select the .html file you saved when using the exports feature. +

    +
    +
    +
    \ No newline at end of file diff --git a/app/plugins/bookmarks/views/tutorials/brave/pin.html b/app/plugins/bookmarks/views/tutorials/brave/pin.html new file mode 100644 index 0000000..5af950d --- /dev/null +++ b/app/plugins/bookmarks/views/tutorials/brave/pin.html @@ -0,0 +1,36 @@ +
    +
    +
    +

    How to pin the extension to your toolbar in Brave

    + {tutorialCrumbs} +
    + +

    + Once installed, you can add the extension to your brave toolbar with just a few easy steps. +

    +
    Step 1: Find the extensions icon on your toolbar.
    +

    + It should look like this: +

    +
    + Right-clicking on the extension icon +
    +
    Step 2: Left click this icon and a list of your currently installed extensions should show.
    +

    + Next to AllTheBookmarks, on the right you should see a pin icon, click this. +

    +
    + Extension settings page +
    +

    + Once pinned, you should see the AllTheBookmarks icon on the toolbar. +

    +
    + Extension settings page +
    +
    +
    +
    \ No newline at end of file diff --git a/app/plugins/bookmarks/views/tutorials/brave/settings.html b/app/plugins/bookmarks/views/tutorials/brave/settings.html new file mode 100644 index 0000000..d017579 --- /dev/null +++ b/app/plugins/bookmarks/views/tutorials/brave/settings.html @@ -0,0 +1,34 @@ +
    +
    +
    +

    How to Access Extension Settings in Brave

    + {tutorialCrumbs} +
    + +

    + With Brave, accessing the extension settings couldn't be easier. +

    +
    Step 1: Locate the Extension Icon
    +

    + Simply locate the AllTheBookmarks icon on your toolbar and right-click the icon. +

    +
    + Right-clicking on the extension icon +
    +
    Step 2: Open the Options
    +

    + From the dropdown menu that appears, select Options. You should be greeted by a settings page very similar to the one below. +

    +
    + Extension settings page +
    +

    + (If you need help pinning the extension to your toolbar for easy access, we have a separate + tutorial for that.) +

    +
    +
    +
    \ No newline at end of file diff --git a/app/plugins/bookmarks/views/tutorials/card.html b/app/plugins/bookmarks/views/tutorials/card.html new file mode 100644 index 0000000..9242450 --- /dev/null +++ b/app/plugins/bookmarks/views/tutorials/card.html @@ -0,0 +1,38 @@ +
    +

    {pretty} Tutorials

    + +

\ No newline at end of file diff --git a/app/plugins/bookmarks/views/tutorials/chrome/export.html b/app/plugins/bookmarks/views/tutorials/chrome/export.html new file mode 100644 index 0000000..eb844c0 --- /dev/null +++ b/app/plugins/bookmarks/views/tutorials/chrome/export.html @@ -0,0 +1,43 @@ +
+
+
+

How to Export bookmarks from Chrome

+ {tutorialCrumbs} +
+

+ Exporting your bookmarks is lightning fast and simple inside of chrome. +

+
Step 1: open the main application menu
+

+ Simply left-click these three dots on the right side of your main toolbar. +

+
+ Right-clicking on the extension icon +
+
Step 2: Open the Bookmark Manager
+

+ In the list, hover over Bookmarks and Lists then select Bookmark Manager. +

+
+ Extension settings page +
+
Step 2: Open the Export Menu
+

+ On this page, in the top right, you should see another set of three dots, click these. +

+
+ Extension settings page +
+

+ From this list, select Export Bookmarks. +

+
+ Extension settings page +
+ +

+ Finally, save the .html file somewhere easy to access when you use the imports feature. +

+
+
+
\ No newline at end of file diff --git a/app/plugins/bookmarks/views/tutorials/chrome/import.html b/app/plugins/bookmarks/views/tutorials/chrome/import.html new file mode 100644 index 0000000..73bc1bf --- /dev/null +++ b/app/plugins/bookmarks/views/tutorials/chrome/import.html @@ -0,0 +1,45 @@ +
+
+
+

How to Import bookmarks in Chrome

+ {tutorialCrumbs} +
+

+ Importing your bookmarks from AllTheBookmarks is lightning fast and simple inside of chrome. +

+
Step 1: open the main application menu
+

+ Simply left-click these three dots on the right side of your main toolbar. +

+
+ Right-clicking on the extension icon +
+
Step 2: Open the Bookmark Manager
+

+ In the list, hover over Bookmarks and Lists then select Bookmark Manager. +

+
+ Extension settings page +
+
Step 2: Open the Import Menu
+ +

+ On this page, in the top right, you should see another set of three dots, click these. +

+
+ Extension settings page +
+ +

+ From this list, select Import Bookmarks. +

+
+ Extension settings page +
+ +

+ Finally, select the .html file you saved when using the exports feature. +

+
+
+
\ No newline at end of file diff --git a/app/plugins/bookmarks/views/tutorials/chrome/pin.html b/app/plugins/bookmarks/views/tutorials/chrome/pin.html new file mode 100644 index 0000000..8f963db --- /dev/null +++ b/app/plugins/bookmarks/views/tutorials/chrome/pin.html @@ -0,0 +1,36 @@ +
+
+
+

How to pin the extension to your toolbar in Chrome

+ {tutorialCrumbs} +
+ +

+ Once installed, you can add the extension to your chrome toolbar with just a few easy steps. +

+
Step 1: Find the extensions icon on your toolbar.
+

+ It should look like this: +

+
+ Right-clicking on the extension icon +
+
Step 2: Left click this icon and a list of your currently installed extensions should show.
+

+ Next to AllTheBookmarks, on the right you should see a pin icon, click this. +

+
+ Extension settings page +
+

+ Once pinned, you should see the AllTheBookmarks icon on the toolbar. +

+
+ Extension settings page +
+
+
+
\ No newline at end of file diff --git a/app/plugins/bookmarks/views/tutorials/chrome/settings.html b/app/plugins/bookmarks/views/tutorials/chrome/settings.html new file mode 100644 index 0000000..98df519 --- /dev/null +++ b/app/plugins/bookmarks/views/tutorials/chrome/settings.html @@ -0,0 +1,34 @@ +
+
+
+

How to Access Extension Settings in Chrome

+ {tutorialCrumbs} +
+ +

+ With Chrome, accessing the extension settings couldn't be easier. +

+
Step 1: Locate the Extension Icon
+

+ Simply locate the AllTheBookmarks icon on your toolbar and right-click the icon. +

+
+ Right-clicking on the extension icon +
+
Step 2: Open the Options
+

+ From the dropdown menu that appears, select Options. You should be greeted by a settings page very similar to the one below. +

+
+ Extension settings page +
+

+ (If you need help pinning the extension to your toolbar for easy access, we have a separate + tutorial for that.) +

+
+
+
\ No newline at end of file diff --git a/app/plugins/bookmarks/views/tutorials/edge/export.html b/app/plugins/bookmarks/views/tutorials/edge/export.html new file mode 100644 index 0000000..c5d5265 --- /dev/null +++ b/app/plugins/bookmarks/views/tutorials/edge/export.html @@ -0,0 +1,48 @@ +
+
+
+

How to Export bookmarks in Edge

+ {tutorialCrumbs} +
+

+ Exporting your bookmarks is lightning fast and simple inside of Edge. +

+ +
Step 1: open the main application menu
+

+ Simply left-click these three dots on the right side of your main toolbar. +

+
+ Right-clicking on the extension icon +
+ +
Step 2: Open the Bookmark Menu
+

+ In the list, select Favorites and another menu should appear. +

+
+ Extension settings page +
+ +
Step 3: Open the Favorites Options
+

+ In this menu, on the top right click the three dots for a final dropdown. +

+
+ Extension settings page +
+ +
Step 4: Export your bookmarks
+

+ From this list, select Export favorites and you should be taken to the import page. +

+
+ Extension settings page +
+ +

+ Finally, save the .html file somewhere easy to access when you use the imports feature. +

+
+
+
\ No newline at end of file diff --git a/app/plugins/bookmarks/views/tutorials/edge/import.html b/app/plugins/bookmarks/views/tutorials/edge/import.html new file mode 100644 index 0000000..44f5aca --- /dev/null +++ b/app/plugins/bookmarks/views/tutorials/edge/import.html @@ -0,0 +1,64 @@ +
+
+
+

How to Import bookmarks in Edge

+ {tutorialCrumbs} +
+

+ Importing your bookmarks from AllTheBookmarks is lightning fast and simple inside of Edge. +

+ +
Step 1: open the main application menu
+

+ Simply left-click these three dots on the right side of your main toolbar. +

+
+ Right-clicking on the extension icon +
+ +
Step 2: Open the Bookmark Menu
+

+ In the list, select Favorites and another menu should appear. +

+
+ Extension settings page +
+ +
Step 3: Open the Favorites Options
+

+ In this menu, on the top right click the three dots for a final dropdown. +

+
+ Extension settings page +
+ +
Step 4: Open the Import Page
+

+ From this list, select Import favorites and you should be taken to the import page. +

+
+ Extension settings page +
+ +
Step 5: Open the Importer
+

+ On this page, under the title Import from other browsers click the button labeled Choose what to import. +

+
+ Extension settings page +
+ +
Step 6: Begin Importing
+

+ Under Import from make sure to select Favorites or bookmarks HTML file. +

+
+ Extension settings page +
+ +

+ Finally, select the .html file you saved when using the exports feature. +

+
+
+
\ No newline at end of file diff --git a/app/plugins/bookmarks/views/tutorials/edge/pin.html b/app/plugins/bookmarks/views/tutorials/edge/pin.html new file mode 100644 index 0000000..57efd73 --- /dev/null +++ b/app/plugins/bookmarks/views/tutorials/edge/pin.html @@ -0,0 +1,36 @@ +
+
+
+

How to pin the extension to your toolbar in Edge

+ {tutorialCrumbs} +
+ +

+ Once installed, you can add the extension to your Edge toolbar with just a few easy steps. +

+
Step 1: Find the extensions icon on your toolbar.
+

+ It should look like this: +

+
+ Right-clicking on the extension icon +
+
Step 2: Left click this icon and a list of your currently installed extensions should show.
+

+ Next to AllTheBookmarks, on the right you should see an eye icon, click this. +

+
+ Extension settings page +
+

+ Once pinned, you should see the AllTheBookmarks icon on the toolbar. +

+
+ Extension settings page +
+
+
+
\ No newline at end of file diff --git a/app/plugins/bookmarks/views/tutorials/edge/settings.html b/app/plugins/bookmarks/views/tutorials/edge/settings.html new file mode 100644 index 0000000..4f4a0c3 --- /dev/null +++ b/app/plugins/bookmarks/views/tutorials/edge/settings.html @@ -0,0 +1,37 @@ +
+
+
+

How to Access Extension Settings in Edge

+ {tutorialCrumbs} +
+ +

+ With Edge, accessing the extension settings couldn't be easier. +

+ +
Step 1: Locate the Extension Icon
+

+ Simply locate the AllTheBookmarks icon on your toolbar and right-click the icon. +

+
+ Right-clicking on the extension icon +
+ +
Step 2: Open the Options
+

+ From the dropdown menu that appears, select Extension Options. You should be greeted by a settings page very similar to the one below. +

+
+ Extension settings page +
+ +

+ (If you need help pinning the extension to your toolbar for easy access, we have a separate + tutorial for that.) +

+
+
+
\ No newline at end of file diff --git a/app/plugins/bookmarks/views/tutorials/firefox/export.html b/app/plugins/bookmarks/views/tutorials/firefox/export.html new file mode 100644 index 0000000..27d0a97 --- /dev/null +++ b/app/plugins/bookmarks/views/tutorials/firefox/export.html @@ -0,0 +1,55 @@ +
+
+
+

How to Export bookmarks in Firefox

+ {tutorialCrumbs} +
+

+ Exporting your bookmarks is lightning fast and simple inside of Firefox. +

+ +
Step 1: open the main application menu
+

+ Simply left-click these three lines on the right side of your main toolbar. +

+
+ Right-clicking on the extension icon +
+ +
Step 2: Open the Bookmark Menu
+

+ In the list, select Bookmarks and another menu should appear. +

+
+ Extension settings page +
+ +
Step 3: Open the Bookmark Manager
+

+ In the list, select Manage Bookmarks. +

+
+ Extension settings page +
+ +
Step 4: Open the Export Menu
+

+ On this page, near the top left, you should see a button that says Import and Backup. Click this button and another menu should appear. +

+
+ Extension settings page +
+ +

+ From this list, select Export Bookmarks. +

+
+ Extension settings page +
+ +

+ Finally, save the .html file somewhere easy to access when you use the imports feature. +

+
+
+
\ No newline at end of file diff --git a/app/plugins/bookmarks/views/tutorials/firefox/import.html b/app/plugins/bookmarks/views/tutorials/firefox/import.html new file mode 100644 index 0000000..e6f6587 --- /dev/null +++ b/app/plugins/bookmarks/views/tutorials/firefox/import.html @@ -0,0 +1,55 @@ +
+
+
+

How to Import bookmarks in Firefox

+ {tutorialCrumbs} +
+

+ Importing your bookmarks from AllTheBookmarks is lightning fast and simple inside of firefox. +

+ +
Step 1: open the main application menu
+

+ Simply left-click these three lines on the right side of your main toolbar. +

+
+ Right-clicking on the extension icon +
+ +
Step 2: Open the Bookmark Menu
+

+ In the list, select Bookmarks and another menu should appear. +

+
+ Extension settings page +
+ +
Step 3: Open the Bookmark Manager
+

+ In the list, select Manage Bookmarks. +

+
+ Extension settings page +
+ +
Step 3: Open the Import Menu
+

+ On this page, near the top left, you should see a button that says Import and Backup. Click this button and another menu should appear. +

+
+ Extension settings page +
+ +

+ From this list, select Import Bookmarks. +

+
+ Extension settings page +
+ +

+ Finally, select the .html file you saved when using the exports feature. +

+
+
+
\ No newline at end of file diff --git a/app/plugins/bookmarks/views/tutorials/firefox/pin.html b/app/plugins/bookmarks/views/tutorials/firefox/pin.html new file mode 100644 index 0000000..54b3f31 --- /dev/null +++ b/app/plugins/bookmarks/views/tutorials/firefox/pin.html @@ -0,0 +1,36 @@ +
+
+
+

How to pin the extension to your toolbar in Firefox

+ {tutorialCrumbs} +
+ +

+ Once installed, you can add the extension to your firefox toolbar with just a few easy steps. +

+
Step 1: Find the extensions icon on your toolbar.
+

+ It should look like this: +

+
+ Right-clicking on the extension icon +
+
Step 2: Left click this icon and a list of your currently installed extensions should show.
+

+ Next to AllTheBookmarks, on the right you should see a settings icon, click this then select Pin to Toolbar +

+
+ Extension settings page +
+

+ Once pinned, you should see the AllTheBookmarks icon on the toolbar. +

+
+ Extension settings page +
+
+
+
\ No newline at end of file diff --git a/app/plugins/bookmarks/views/tutorials/firefox/settings.html b/app/plugins/bookmarks/views/tutorials/firefox/settings.html new file mode 100644 index 0000000..bd035ba --- /dev/null +++ b/app/plugins/bookmarks/views/tutorials/firefox/settings.html @@ -0,0 +1,52 @@ +
+
+
+

How to Access Extension Settings in Firefox

+ {tutorialCrumbs} +
+ +

+ With Firefox, accessing the extension settings is a breeze. +

+ +
Step 1: Locate the Extension Icon
+

+ Simply locate the AllTheBookmarks icon on your toolbar and right-click the icon. +

+
+ Right-clicking on the extension icon +
+ +
Step 2: Open the Add-on manage page
+

+ From the dropdown menu that appears, select Manage Extension. +

+
+ Extension settings page +
+ +
Step 3: Open the extension setting page
+

+ The extension management page should appear similar to this. In the top right, click these three dots and select Options. +

+
+ Extension settings page +
+ +

+ You should be greeted by a settings page very similar to the one below. +

+
+ Extension settings page +
+ +

+ (If you need help pinning the extension to your toolbar for easy access, we have a separate + tutorial for that.) +

+
+
+
\ No newline at end of file diff --git a/app/plugins/bookmarks/views/tutorials/firefox/update.html b/app/plugins/bookmarks/views/tutorials/firefox/update.html new file mode 100644 index 0000000..e69de29 diff --git a/app/plugins/bookmarks/views/tutorials/list.html b/app/plugins/bookmarks/views/tutorials/list.html new file mode 100644 index 0000000..2b7883a --- /dev/null +++ b/app/plugins/bookmarks/views/tutorials/list.html @@ -0,0 +1,247 @@ +
+

Mobile Tutorials

+
+ + + + +
+ +

Browser Tutorials

+
+ + + + + + + + + + + + + + + + + + {LOGGEDIN} + {BKMTUTS} +

+ The Main menu link to this page can be hidden in your Profile Settings. +

+ {/BKMTUTS} + {/LOGGEDIN} +
+
\ No newline at end of file diff --git a/app/plugins/bookmarks/views/tutorials/mobile/android.html b/app/plugins/bookmarks/views/tutorials/mobile/android.html new file mode 100644 index 0000000..4fc52da --- /dev/null +++ b/app/plugins/bookmarks/views/tutorials/mobile/android.html @@ -0,0 +1,33 @@ +
+
+
+

How to add the app to your Android home-screen.

+ {tutorialCrumbs} +
+
Step 1: Open https://allthebookmarks.com in Chrome.
+

+ from this page, tap the menu icon (three dots) in the top-right corner. +

+
+ Extension settings page +
+
Step 2: Select "Add to Home screen" and a popup will let you choose the name.
+

+ Next, tap "Add". +

+
+ Extension settings page +
+
Step 3: A popup will let you drag the icon around or you can simply select "Add to Home screen".
+
+ Extension settings page +
+

+ The shortcut will now appear on your home screen. +

+
+ Extension settings page +
+
+
+
\ No newline at end of file diff --git a/app/plugins/bookmarks/views/tutorials/mobile/iphone.html b/app/plugins/bookmarks/views/tutorials/mobile/iphone.html new file mode 100644 index 0000000..ab1e479 --- /dev/null +++ b/app/plugins/bookmarks/views/tutorials/mobile/iphone.html @@ -0,0 +1,24 @@ +
+
+
+

How to add the app to your iPhone home-screen

+ {tutorialCrumbs} +
+
Step 1: Open https://allthebookmarks.com in Safari.
+ (this is required; Chrome doesn’t support adding shortcuts on iOS) +
Step 2: Tap the Share icon (square with an upward arrow).
+

+ Next, Scroll down and select "Add to Home Screen" then tap "Add". +

+
+ Extension settings page +
+

+ The shortcut will appear on your home screen. +

+
+ Extension settings page +
+
+
+
\ No newline at end of file diff --git a/app/plugins/bookmarks/views/tutorials/mobileCard.html b/app/plugins/bookmarks/views/tutorials/mobileCard.html new file mode 100644 index 0000000..d920f19 --- /dev/null +++ b/app/plugins/bookmarks/views/tutorials/mobileCard.html @@ -0,0 +1,28 @@ +
+

{pretty} Tutorials

+ +
\ No newline at end of file diff --git a/app/plugins/bookmarks/views/tutorials/opera/export.html b/app/plugins/bookmarks/views/tutorials/opera/export.html new file mode 100644 index 0000000..3f48829 --- /dev/null +++ b/app/plugins/bookmarks/views/tutorials/opera/export.html @@ -0,0 +1,40 @@ +
+
+
+

How to Export bookmarks in Opera

+ {tutorialCrumbs} +
+

+ Exporting your bookmarks is lightning fast and simple inside of Opera. +

+ +
Step 1: open the main application menu
+

+ Simply left-click these three dots in the bottom left of the browser window. +

+
+ Right-clicking on the extension icon +
+ +
Step 2: Open the Bookmark Menu
+

+ In the list, click the icon next to Bookmarks and another window should open. +

+
+ Extension settings page +
+ +
Step 4: Export your bookmarks
+

+ In the bottom right of the page, click Import / Export and select Export Bookmarks. +

+
+ Extension settings page +
+ +

+ Finally, save the .html file somewhere easy to access when you use the imports feature. +

+
+
+
\ No newline at end of file diff --git a/app/plugins/bookmarks/views/tutorials/opera/import.html b/app/plugins/bookmarks/views/tutorials/opera/import.html new file mode 100644 index 0000000..ecf5dc1 --- /dev/null +++ b/app/plugins/bookmarks/views/tutorials/opera/import.html @@ -0,0 +1,48 @@ +
+
+
+

How to Import bookmarks in Opera

+ {tutorialCrumbs} +
+

+ Importing your bookmarks from AllTheBookmarks is lightning fast and simple inside of Opera. +

+ +
Step 1: open the main application menu
+

+ Simply left-click these three dots in the bottom left of the browser window. +

+
+ Right-clicking on the extension icon +
+ +
Step 2: Open the Bookmark Menu
+

+ In the list, click the icon next to Bookmarks and another window should open. +

+
+ Extension settings page +
+ +
Step 3: Open the Import Page
+

+ In the bottom right of the page, click Import / Export and select Import Bookmarks and you should be taken to the import page. +

+
+ Extension settings page +
+ +
Step 4: Begin Importing
+

+ In this menu, click the dropdown and make sure to select Bookmarks HTML file. +

+
+ Extension settings page +
+ +

+ Finally, select the .html file you saved when using the exports feature. +

+
+
+
\ No newline at end of file diff --git a/app/plugins/bookmarks/views/tutorials/opera/pin.html b/app/plugins/bookmarks/views/tutorials/opera/pin.html new file mode 100644 index 0000000..b78d955 --- /dev/null +++ b/app/plugins/bookmarks/views/tutorials/opera/pin.html @@ -0,0 +1,46 @@ +
+
+
+

How to pin the extension to your toolbar in Opera

+ {tutorialCrumbs} +
+ +

+ Once installed, you can add the extension to your Opera toolbar with just a few easy steps. +

+
Step 1: Find the extensions icon on your toolbar.
+

+ It should look like this: +

+
+ Right-clicking on the extension icon +
+ +
Step 0: Reveal the extensions toolbar. ( This step may not be required for all users ).
+

+ If you find that you are having trouble locating the extensions icon, double check that the additional toolbar is expanded. +

+
+ Right-clicking on the extension icon +
+ +
Step 2: Left click this icon and a list of your currently installed extensions should show.
+

+ Next to AllTheBookmarks, on the right you should see an eye icon, click this. +

+
+ Extension settings page +
+ +

+ Once pinned, you should see the AllTheBookmarks icon on the toolbar. +

+
+ Extension settings page +
+
+
+
\ No newline at end of file diff --git a/app/plugins/bookmarks/views/tutorials/opera/settings.html b/app/plugins/bookmarks/views/tutorials/opera/settings.html new file mode 100644 index 0000000..028c65a --- /dev/null +++ b/app/plugins/bookmarks/views/tutorials/opera/settings.html @@ -0,0 +1,37 @@ +
+
+
+

How to Access Extension Settings in Opera

+ {tutorialCrumbs} +
+ +

+ With Opera, accessing the extension settings couldn't be easier. +

+ +
Step 1: Locate the Extension Icon
+

+ Simply locate the AllTheBookmarks icon on your toolbar and right-click the icon. +

+
+ Right-clicking on the extension icon +
+ +
Step 2: Open the Options
+

+ From the dropdown menu that appears, select Options. You should be greeted by a settings page very similar to the one below. +

+
+ Extension settings page +
+ +

+ (If you need help pinning the extension to your toolbar for easy access, we have a separate + tutorial for that.) +

+
+
+
\ No newline at end of file diff --git a/app/plugins/bugreport/views/create.html b/app/plugins/bugreport/views/create.html index 3f6ca83..bfa5c14 100644 --- a/app/plugins/bugreport/views/create.html +++ b/app/plugins/bugreport/views/create.html @@ -54,7 +54,7 @@
- +
diff --git a/app/plugins/comments/views/create.html b/app/plugins/comments/views/create.html index 6adaf54..0076cd9 100644 --- a/app/plugins/comments/views/create.html +++ b/app/plugins/comments/views/create.html @@ -8,7 +8,7 @@ id="comment" placeholder="Write your comment here..."> - + diff --git a/app/plugins/contact/views/create.html b/app/plugins/contact/views/create.html index c4783ba..63624a0 100644 --- a/app/plugins/contact/views/create.html +++ b/app/plugins/contact/views/create.html @@ -38,10 +38,10 @@ - -
- -
- + +
+ +
+ \ No newline at end of file diff --git a/app/plugins/members/controllers/admin/invoices.php b/app/plugins/members/controllers/admin/invoices.php new file mode 100644 index 0000000..7d5baba --- /dev/null +++ b/app/plugins/members/controllers/admin/invoices.php @@ -0,0 +1,50 @@ + + * @link https://TheTempusProject.com + * @license https://opensource.org/licenses/MIT [MIT LICENSE] + */ +namespace TheTempusProject\Controllers\Admin; + +use TheTempusProject\Houdini\Classes\Views; +use TheTempusProject\Houdini\Classes\Navigation; +use TheTempusProject\Houdini\Classes\Components; +use TheTempusProject\Classes\AdminController; +use TheTempusProject\Models\MembershipRecords as MemberModel; + +class Invoices extends AdminController { + public static $memberships; + + public function __construct() { + parent::__construct(); + self::$title = 'Admin - Memberships'; + self::$memberships = new MemberModel; + $view = Navigation::activePageSelect( 'nav.admin', '/admin/member' ); + Components::set( 'ADMINNAV', $view ); + } + + public function index( $data = null ) { + Views::view( 'members.admin.list', self::$memberships->list() ); + } + + public function create( $data = null ) { + } + + public function edit( $data = null ) { + } + + public function view( $data = null ) { + } + + public function delete( $data = null ) { + } + + public function preview( $data = null ) { + } +} diff --git a/app/plugins/members/controllers/admin/members.php b/app/plugins/members/controllers/admin/members.php new file mode 100644 index 0000000..944acdf --- /dev/null +++ b/app/plugins/members/controllers/admin/members.php @@ -0,0 +1,75 @@ + + * @link https://TheTempusProject.com + * @license https://opensource.org/licenses/MIT [MIT LICENSE] + */ +namespace TheTempusProject\Controllers\Admin; + +use TheTempusProject\Houdini\Classes\Views; +use TheTempusProject\Houdini\Classes\Navigation; +use TheTempusProject\Houdini\Classes\Components; +use TheTempusProject\Classes\AdminController; +use TheTempusProject\Plugins\Members as MemberModel; +use TheTempusProject\Houdini\Classes\Issues; +use TheTempusProject\Bedrock\Functions\Input; +use TheTempusProject\Hermes\Functions\Route as Routes; +use TheTempusProject\Hermes\Functions\Redirect; + +class Members extends AdminController { + public function __construct() { + parent::__construct(); + $view = Navigation::activePageSelect( 'nav.admin', '/admin/member' ); + Components::set( 'ADMINNAV', $view ); + } + + public function index( $data = null ) { + self::$title = 'Admin - Membership Scripts'; + Views::view( 'members.admin.scripts' ); + } + + public function orphans( $data = null, $id = null ) { + self::$title = 'Admin - Orphaned PRoducts'; + if ( $data = 'abandon' && ! empty( $id ) ) { + MemberModel::orphanAbandon( $id ); + Redirect::to('admin/members/orphans'); + } + $orphans = MemberModel::findOrphans(); + Views::view( 'members.admin.orphans', $orphans ); + } + + public function webhooks( $data = null, $id = null ) { + self::$title = 'Admin - Membership Webhooks'; + + if ( $data = 'delete' && ! empty( $id ) ) { + MemberModel::webhookRemove( $id ); + Redirect::to('admin/members/webhooks'); + } + + $data = []; + $webhooks = MemberModel::webhookList(); + foreach ($webhooks->data as $key => $webhook) { + $hook = new \stdClass; + $hook->id = $webhook->id; + $hook->enabled_events = implode( ',
', $webhook->enabled_events ); + $hook->status = $webhook->status; + $hook->url = $webhook->url; + $data[] = $hook; + } + Components::set( 'urltouse', Routes::getAddress() ); + + if ( !Input::exists( 'submit' ) ) { + return Views::view( 'members.admin.webhooks', $data ); + } + MemberModel::webhookSetup(); + Issues::add( 'success', 'Webhooks Generated' ); + Issues::add( 'error', 'Now, LEAVE!' ); + Redirect::to('admin/members/webhooks'); + } +} diff --git a/app/plugins/members/controllers/admin/products.php b/app/plugins/members/controllers/admin/products.php new file mode 100644 index 0000000..b40e80b --- /dev/null +++ b/app/plugins/members/controllers/admin/products.php @@ -0,0 +1,97 @@ + + * @link https://TheTempusProject.com + * @license https://opensource.org/licenses/MIT [MIT LICENSE] + */ +namespace TheTempusProject\Controllers\Admin; + +use TheTempusProject\Houdini\Classes\Views; +use TheTempusProject\Houdini\Classes\Navigation; +use TheTempusProject\Houdini\Classes\Components; +use TheTempusProject\Classes\AdminController; +use TheTempusProject\Models\MembershipProducts; +use TheTempusProject\Bedrock\Functions\Input; +use TheTempusProject\Classes\Forms; +use TheTempusProject\Houdini\Classes\Issues; +use TheTempusProject\Bedrock\Functions\Check; + +class Products extends AdminController { + public static $products; + + public function __construct() { + parent::__construct(); + self::$title = 'Admin - Membership Products'; + self::$products = new MembershipProducts; + $view = Navigation::activePageSelect( 'nav.admin', '/admin/products' ); + Components::set( 'ADMINNAV', $view ); + } + + public function index( $data = null ) { + Views::view( 'members.admin.products.list', self::$products->list() ); + } + + public function create( $data = null ) { + if ( !Input::exists( 'submit' ) ) { + return Views::view( 'members.admin.products.create' ); + } + if ( !Forms::check( 'newMembershipProduct' ) ) { + Issues::add( 'error', [ 'There was an error with your request.' => Check::userErrors() ] ); + return $this->index(); + } + $result = self::$products->create( Input::post( 'name' ), Input::post( 'description' ), Input::post( 'monthly_price' ), Input::post( 'yearly_price' ) ); + if ( $result ) { + Issues::add( 'success', 'Your product has been created.' ); + return $this->index(); + } else { + Issues::add( 'error', [ 'There was an unknown error submitting your data.' => Check::userErrors() ] ); + return $this->index(); + } + } + + public function edit( $id = null ) { + if ( !Input::exists( 'submit' ) ) { + return Views::view( 'members.admin.products.edit', self::$products->findById( $id ) ); + } + if ( !Forms::check( 'editMembershipProduct' ) ) { + Issues::add( 'error', [ 'There was an error with your form.' => Check::userErrors() ] ); + return $this->index(); + } + + if ( self::$products->updatePost( $id, Input::post( 'name' ), Input::post( 'description' ), Input::post( 'monthly_price' ), Input::post( 'yearly_price' ) ) === true ) { + Issues::add( 'success', 'Your product Updated.' ); + return $this->index(); + } + Issues::add( 'error', 'There was an error with your request.' ); + $this->index(); + } + + public function view( $id = null ) { + $data = self::$products->findById( $id ); + if ( $data !== false ) { + return Views::view( 'members.admin.products.view', $data ); + } + Issues::add( 'error', 'Product not found.' ); + $this->index(); + } + + public function delete( $data = null ) { + if ( $data == null ) { + if ( Input::exists( 'MP_' ) ) { + $data = Input::post( 'MP_' ); + } + } + if ( !self::$products->delete( (array) $data ) ) { + Issues::add( 'error', 'There was an error with your request.' ); + } else { + Issues::add( 'success', 'Post has been deleted' ); + } + $this->index(); + } +} diff --git a/app/plugins/members/controllers/admin/records.php b/app/plugins/members/controllers/admin/records.php new file mode 100644 index 0000000..8266ee8 --- /dev/null +++ b/app/plugins/members/controllers/admin/records.php @@ -0,0 +1,56 @@ + + * @link https://TheTempusProject.com + * @license https://opensource.org/licenses/MIT [MIT LICENSE] + */ +namespace TheTempusProject\Controllers\Admin; + +use TheTempusProject\Houdini\Classes\Views; +use TheTempusProject\Houdini\Classes\Navigation; +use TheTempusProject\Houdini\Classes\Components; +use TheTempusProject\Classes\AdminController; +use TheTempusProject\Models\Memberships as MemberModel; + +class Records extends AdminController { + public static $memberships; + + public function __construct() { + parent::__construct(); + self::$title = 'Admin - Memberships'; + self::$memberships = new MemberModel; + $view = Navigation::activePageSelect( 'nav.admin', '/admin/member' ); + Components::set( 'ADMINNAV', $view ); + } + + public function index( $data = null ) { + Views::view( 'members.admin.memberships.list', self::$memberships->list() ); + } + + public function create( $data = null ) { + } + + public function edit( $data = null ) { + } + + public function view( $id = null ) { + $data = self::$memberships->findById( $id ); + if ( $data !== false ) { + return Views::view( 'members.admin.memberships.view', $data ); + } + Issues::add( 'error', 'Membership not found.' ); + $this->index(); + } + + public function delete( $data = null ) { + } + + public function preview( $data = null ) { + } +} diff --git a/app/plugins/members/controllers/api/stripe.php b/app/plugins/members/controllers/api/stripe.php new file mode 100644 index 0000000..392e4c2 --- /dev/null +++ b/app/plugins/members/controllers/api/stripe.php @@ -0,0 +1,188 @@ + + * @link https://TheTempusProject.com + * @license https://opensource.org/licenses/MIT [MIT LICENSE] + */ +namespace TheTempusProject\Controllers\Api; + +use Stripe\StripeClient; +use Stripe\Event; +use TheTempusProject\Models\User; +use TheTempusProject\Classes\ApiController; +use TheTempusProject\Houdini\Classes\Views; +use TheTempusProject\Bedrock\Classes\Config; +use TheTempusProject\Canary\Bin\Canary as Debug; +use TheTempusProject\Models\MembershipCustomers; +use TheTempusProject\Models\Memberships; + +class Stripe extends ApiController { + public static $stripe; + public static $customers; + public static $memberships; + + public function __construct() { + parent::__construct( false ); + $api_key = Config::getValue( 'memberships/stripeSecret' ); + self::$stripe = new StripeClient($api_key); + } + + public function webhook() { + try { + $payload = @file_get_contents('php://input'); + $payload = json_decode( $payload, true ); + if ( ! is_array( $payload ) ) { + throw new \Exception("Error Processing Request", 1); + } + $event = null; + $event = Event::constructFrom( $payload ); + + $eventData = $event->data->object; + + // $event->type gives the event type obv + switch ($event->type) { + // case 'invoice.paid': + // Debug::error( 'processing: ' . $event->type ); + // "id": "in_1QSDcJGsigymNdIJo0Z1a20K", + // "customer": "cus_RKtOtR7X7CwPRU", + // "charge": "ch_3QSDcJGsigymNdIJ0Smb7Rmx", + // "subscription": "sub_1QSDcJGsigymNdIJWGw7Zrv9", + // break; + + // case 'charge.succeeded': + // Debug::error( 'processing: ' . $event->type ); + // "id": "ch_3QSDcJGsigymNdIJ0Smb7Rmx", + // "invoice": "in_1QSDcJGsigymNdIJo0Z1a20K", + // "status": "succeeded", + // break; + + case 'customer.subscription.updated': + case 'customer.subscription.paused': + case 'customer.subscription.resumed': + case 'customer.subscription.deleted': + Debug::error( 'processing: ' . $event->type ); + + self::$memberships = new Memberships; + $membership_id = self::$memberships->findBySubscriptionID( $eventData->id ); + if ( empty( $membership_id ) ) { + Debug::error( 'membership not found' ); + + self::$customers = new MembershipCustomers; + $customer = self::$customers->findByCustomerID( $eventData->customer ); + if ( empty( $customer ) ) { + Debug::error( 'customer not found' ); + Debug::v( $eventData->customer ); + break; + } + + $result = self::$memberships->create( + $eventData->customer, + $eventData->id, + $eventData->plan->id, + $eventData->current_period_start, + $eventData->current_period_end, + $eventData->status, + $customer->local_user, + 'frequency' + ); + } else { + $result = self::$memberships->update( $membership_id->ID, $eventData->current_period_start, $eventData->current_period_end, $eventData->status ); + } + + if ( empty( $result ) ) { + Debug::error( 'membership not updated' ); + Debug::v( $result ); + } + break; + case 'customer.subscription.created': + Debug::error( 'processing: ' . $event->type ); + + self::$memberships = new Memberships; + $membership_id = self::$memberships->findBySubscriptionID( $eventData->id ); + if ( ! empty( $membership_id ) ) { + Debug::error( 'subscription already created' ); + break; + } + self::$customers = new MembershipCustomers; + $customer = self::$customers->findByCustomerID( $eventData->customer ); + if ( empty( $customer ) ) { + Debug::error( 'customer not found' ); + Debug::v( $eventData->customer ); + break; + } + Debug::error( 'processing: ' . $event->type ); + + $result = self::$memberships->create( + $eventData->customer, + $eventData->id, + $eventData->plan->id, + $eventData->current_period_start, + $eventData->current_period_end, + $eventData->status, + $customer->local_user, + 'frequency' + ); + Debug::error( 'processing: ' . $event->type ); + + if ( empty( $result ) ) { + Debug::error( 'membership not made' ); + Debug::v( $result ); + } + Debug::error( 'done processing: ' . var_export($result,true) ); + break; + // case 'invoice.created': + // Debug::error( 'processing: ' . $event->type ); + // happens when the payment first starts + + // "id": "in_1QSDcJGsigymNdIJo0Z1a20K", + // "customer": "cus_RKtOtR7X7CwPRU", + // "status": "open", + // "total": 888, + // break; + // case 'checkout.session.completed': + // Debug::error( 'processing: ' . $event->type ); + // new customer has completed first checkout + // add thier record or update iit + + // "invoice": "in_1QSDcJGsigymNdIJo0Z1a20K", + // "mode": "subscription", + // "status": "complete", + // "customer": "cus_RKtOtR7X7CwPRU", + // break; + // case 'payment_intent.succeeded': + // Debug::error( 'processing: ' . $event->type ); + // $paymentIntent = $event->data->object; + // break; + // case 'payment_method.attached': + // Debug::error( 'processing: ' . $event->type ); + // $paymentMethod = $event->data->object; + // break; + default: + Debug::error( 'Skipped Event:' . $event->type ); + break; + } + + + $responseType = 'success'; + $response = true; + } catch(\UnexpectedValueException $e) { + Debug::error( 'UnexpectedValueException' ); + Debug::v( $e ); + http_response_code(400); + $responseType = 'error'; + $response = 'UnexpectedValueException'; + } catch(\Exception $e) { + Debug::error( 'Exception' ); + Debug::error( $e ); + http_response_code(400); + $responseType = 'error'; + $response = 'Exception'; + } + Views::view( 'api.response', ['response' => json_encode( [ $responseType => $response ], true )]); + } +} \ No newline at end of file diff --git a/app/plugins/members/controllers/member.php b/app/plugins/members/controllers/member.php new file mode 100644 index 0000000..31959bf --- /dev/null +++ b/app/plugins/members/controllers/member.php @@ -0,0 +1,339 @@ + + * @link https://TheTempusProject.com + * @license https://opensource.org/licenses/MIT [MIT LICENSE] + */ +namespace TheTempusProject\Controllers; + +use TheTempusProject\Houdini\Classes\Template; +use TheTempusProject\Houdini\Classes\Views; +use TheTempusProject\Houdini\Classes\Issues; +use TheTempusProject\Classes\Controller; +use TheTempusProject\TheTempusProject as App; +use TheTempusProject\Hermes\Functions\Redirect; +use TheTempusProject\Bedrock\Functions\Session; +use TheTempusProject\Models\MembershipCustomers; +use TheTempusProject\Models\MembershipProducts; +use TheTempusProject\Bedrock\Classes\Config; +use TheTempusProject\Bedrock\Functions\Input; +use Stripe\Checkout\Session as StripeSession; +use TheTempusProject\Hermes\Functions\Route as Routes; +use TheTempusProject\Houdini\Classes\Navigation; +use TheTempusProject\Models\Memberships; +use TheTempusProject\Houdini\Classes\Components; +use TheTempusProject\Classes\Forms; +use TheTempusProject\Bedrock\Functions\Hash; +use TheTempusProject\Canary\Bin\Canary as Debug; +use Stripe\StripeClient; + +class Member extends Controller { + public static $customers; + public static $products; + public static $stripe; + private static $loaded = false; + + public function __construct() { + parent::__construct(); + + if ( ! self::$loaded ) { + Template::noIndex(); + self::$customers = new MembershipCustomers; + self::$products = new MembershipProducts; + $api_key = Config::getValue( 'memberships/stripeSecret' ); + if ( $api_key == 'sk_xxxxxxxxxxxxxxx' || empty($api_key) ) { + Debug::error( "Memberships:__construct No Stripe Key found" ); + } else { + self::$stripe = new StripeClient( $api_key ); + } + self::$loaded = true; + } + } + + public function index() { + $this->confirmAuth(); + self::$title = 'Members Area'; + Views::view( 'members.members' ); + } + + public function managepayment() { + $this->confirmAuth(); + $customer = self::$customers->findByUserID( App::$activeUser->ID ); + if ( empty( $customer ) ) { + Session::flash( 'error', 'You do not have any active payment methods. You can subscribe by going here' ); + return Redirect::to( 'member/manage' ); + } + try { + $session = self::$stripe->billingPortal->sessions->create([ + 'customer' => $customer->stripe_customer, + 'return_url' => Routes::getAddress() . 'member/manage', + ]); + } catch (\Stripe\Exception\InvalidRequestException $e) { + Debug::error('Membership -> ManagePayment - Stripe not configured correctly'); + Debug::error( $e ); + Session::flash( 'error', 'There was an issue redirecting you to Stripe, please try again.' ); + return Redirect::to( 'member/manage' ); + } + + header('Location: ' . $session->url); + exit; + } + + public function cancelconfirm( $id = null ) { + $this->confirmAuth( $id ); + $memberships = new Memberships; + $result = $memberships->cancel( $id ); + if ( ! empty( $result ) ) { + Session::flash( 'success', 'Your Membership has been paused.' ); + Redirect::to( 'member/manage' ); + } else { + Session::flash( 'error', 'There was an error canceling your membership' ); + Redirect::to( 'member/manage' ); + } + } + + public function pauseconfirm( $id = null ) { + $this->confirmAuth( $id ); + $memberships = new Memberships; + $result = $memberships->cancel( $id ); + if ( ! empty( $result ) ) { + Session::flash( 'success', 'Your Membership has been paused.' ); + Redirect::to( 'member/manage' ); + } else { + Session::flash( 'error', 'There was an error canceling your membership' ); + Redirect::to( 'member/manage' ); + } + } + + public function pause( $id = null ) { + $this->confirmAuth( $id ); + self::$title = 'pause Membership'; + Components::set( 'pauseid', $id ); + Views::view( 'members.pause' ); + } + + public function resume( $id = null ) { + $this->confirmAuth( $id ); + self::$title = 'resume Membership'; + Views::view( 'members.resume' ); + } + + public function cancel( $id = null ) { + $this->confirmAuth( $id ); + self::$title = 'Cancel Membership'; + Components::set( 'cancelid', $id ); + Views::view( 'members.cancel' ); + } + + public function manage() { + if ( ! App::$isLoggedIn ) { + Session::flash( 'error', 'You do not have permission to access this page.' ); + return Redirect::home(); + } + self::$title = 'Manage Membership'; + + $menu = Views::simpleView( 'nav.usercp', App::$userCPlinks ); + Navigation::activePageSelect( $menu, null, true, true ); + + $memberships = new Memberships; + $userMemberships = $memberships->getUserSubs(); + Views::view( 'members.manage', $userMemberships ); + } + + public function upgrade() { + if ( ! App::$isLoggedIn ) { + Session::flash( 'error', 'You do not have permission to access this page.' ); + return Redirect::home(); + } + // need to check if the plan CAN be upgraded + self::$title = 'Upgrade Membership'; + Views::view( 'members.upgrade' ); + } + + public function join( $plan = 'monthly' ) { + if ( ! App::$isLoggedIn ) { + Session::flash( 'error', 'You do not have permission to access this page.' ); + return Redirect::home(); + } + $plan = strtolower( $plan ); + if ( ! in_array( $plan, ['monthly','yearly'] ) ) { + Session::flash( 'error', 'Unknown plan' ); + return Redirect::to( 'home/index' ); + } + + self::$title = 'Join {SITENAME}!'; + $stripePrice = $this->findPrice( $plan ); + + $product = self::$products->findByPriceID( $stripePrice ); + if ( empty( $product ) ) { + Session::flash( 'success', 'We aren\'t currently accepting new members, please check back soon!' ); + return Redirect::home(); + } + Views::view( 'members.join', $product ); + } + + public function checkout( $plan = 'monthly' ) { + if ( ! App::$isLoggedIn ) { + Session::flash( 'error', 'You do not have permission to access this page.' ); + return Redirect::home(); + } + $customer = self::$customers->findOrCreate( App::$activeUser->ID ); + if ( empty( $customer ) ) { + Issues::add( 'error', 'no customer' ); + return $this->index(); + } + + $plan = strtolower( $plan ); + if ( ! in_array( $plan, ['monthly','yearly','upgrade'] ) ) { + Session::flash( 'error', 'Unknown plan' ); + return Redirect::to( 'home/index' ); + } + if ( $plan === 'upgrade' ) { + $plan = 'yearly'; + $successUrl = Routes::getAddress() . 'member/payment/upgrade?session_id={CHECKOUT_SESSION_ID}'; + } else { + $successUrl = Routes::getAddress() . 'member/payment/complete?session_id={CHECKOUT_SESSION_ID}'; + } + + $stripePrice = $this->findPrice( $plan ); + + $session = self::$stripe->checkout->sessions->create([ + 'payment_method_types' => ['card'], + 'customer' => $customer, + 'line_items' => [[ + 'price' => $stripePrice, + 'quantity' => 1, + ]], + 'mode' => 'subscription', + 'allow_promotion_codes' => true, + 'success_url' => $successUrl, + 'cancel_url' => Routes::getAddress() . 'member/payment/cancel', + ]); + header('Location: ' . $session->url); + exit; + } + + public function payment( $type = '' ) { + $type = strtolower( $type ); + if ( ! in_array( $type, ['cancel','complete','upgrade'] ) ) { + Session::flash( 'error', 'Unknown Payment' ); + return Redirect::to( 'home/index' ); + } + + if ( $type == 'cancel' ) { + self::$title = '(almost) Members Area'; + return Views::view( 'members.paymentcanceled' ); + } + + if ( $type == 'upgrade' ) { + self::$title = 'Better Members Area'; + return Views::view( 'members.upgradeCompleted' ); + } + + self::$title = '(almost) Members Area'; + Views::view( 'members.paymentcomplete' ); + } + + // This combines a registration with a checkout + public function signup( $plan = 'monthly' ) { + $plan = strtolower( $plan ); + if ( ! in_array( $plan, ['monthly','yearly'] ) ) { + Session::flash( 'error', 'Unknown plan' ); + return Redirect::to( 'home/index' ); + } + + $product = self::$products->mainProduct(); + if ( empty( $product ) ) { + Session::flash( 'error', 'Unknown product' ); + return Redirect::to( 'home/index' ); + } + + $stripePrice = $this->findPrice( $plan ); + + $pretty = 'prettyPrice' . ucfirst( $plan ); + $prettyPrice = $product->$pretty; + + self::$title = 'Sign up for {SITENAME} ' . ucfirst( $plan ); + + Components::set( 'planName', ucfirst( $plan ) ); + Components::set( 'prettyPrice', $prettyPrice ); + Components::set( 'TERMS', Views::simpleView( 'terms' ) ); + + if ( App::$isLoggedIn ) { + Session::flash( 'notice', 'You are already logged in, you can subscribe here, or upgrade here.' ); + return Redirect::to( 'home/index' ); + } + + if ( !Input::exists() ) { + return Views::view( 'members.register' ); + } + + if ( ! Forms::check( 'register' ) ) { + Issues::add( 'error', [ 'There was an error with your registration.' => Check::userErrors() ] ); + return Views::view( 'members.register' ); + } + + self::$user->create( [ + 'username' => Input::post( 'username' ), + 'password' => Hash::make( Input::post( 'password' ) ), + 'email' => Input::post( 'email' ), + 'terms' => 1, + ] ); + + if ( !self::$user->logIn( Input::post( 'username' ), Input::post( 'password' ), Input::post( 'remember' ) ) ) { + Session::flash( 'error', 'Thank you for registering! Unfortunately, there was an issue logging you in, please log in and order again.' ); + return Redirect::to( 'home/index' ); + } + + $user = self::$user->authorize( Input::post( 'username' ), Input::post( 'password' ) ); + + $customer = self::$customers->findOrCreate( $user->ID ); + if ( empty( $customer ) ) { + Session::flash( 'error', 'Thank you for registering! Unfortunately, there was an issue communicating with Stripe and we can\'t collect payment right now.' ); + return Redirect::to( 'home/index' ); + } + $session = self::$stripe->checkout->sessions->create([ + 'payment_method_types' => ['card'], + 'customer' => $customer, + 'line_items' => [[ + 'price' => $stripePrice, + 'quantity' => 1, + ]], + 'mode' => 'subscription', + 'allow_promotion_codes' => true, + 'success_url' => Routes::getAddress() . 'member/payment/complete?session_id={CHECKOUT_SESSION_ID}', + 'cancel_url' => Routes::getAddress() . 'member/payment/cancel', + ]); + header('Location: ' . $session->url); + exit; + } + + private function findPrice( $plan ) { + $plan = strtolower( $plan ); + if ( ! in_array( $plan, ['monthly','yearly'] ) ) { + Session::flash( 'error', 'Unknown plan' ); + return Redirect::to( 'home/index' ); + } + + $product = self::$products->mainProduct(); + if ( empty( $product ) ) { + Session::flash( 'error', 'Unknown product' ); + return Redirect::to( 'home/index' ); + } + $index = 'stripe_price_' . $plan; + $stripePrice = $product->$index; + return $stripePrice; + } + + private function confirmAuth() { + if ( ! App::$isLoggedIn || ! App::$isMember ) { + Session::flash( 'error', 'You do not have permission to access this page.' ); + return Redirect::home(); + } + } +} \ No newline at end of file diff --git a/app/plugins/members/forms.php b/app/plugins/members/forms.php new file mode 100644 index 0000000..295ea2c --- /dev/null +++ b/app/plugins/members/forms.php @@ -0,0 +1,47 @@ + + * @link https://TheTempusProject.com + * @license https://opensource.org/licenses/MIT [MIT LICENSE] + */ +namespace TheTempusProject\Plugins\Subscribe; + +use TheTempusProject\Bedrock\Functions\Input; +use TheTempusProject\Classes\Forms; + +class MembershipForms extends Forms { + /** + * Adds these functions to the form list. + */ + public function __construct() { + self::addHandler( 'newMembershipProduct', __CLASS__, 'newMembershipProduct' ); + self::addHandler( 'editMembershipProduct', __CLASS__, 'editMembershipProduct' ); + } + + /** + * Validates the subscribe form. + * + * @return {bool} + */ + public static function newMembershipProduct() { + // if ( !self::token() ) { + // return false; + // } + return true; + } + public static function editMembershipProduct() { + // if ( !self::token() ) { + // return false; + // } + return true; + } + +} + +new MembershipForms; diff --git a/app/plugins/members/models/membership_customers.php b/app/plugins/members/models/membership_customers.php new file mode 100644 index 0000000..fe1fc7b --- /dev/null +++ b/app/plugins/members/models/membership_customers.php @@ -0,0 +1,104 @@ + + * @link https://TheTempusProject.com + * @license https://opensource.org/licenses/MIT [MIT LICENSE] + */ +namespace TheTempusProject\Models; + +use TheTempusProject\Canary\Bin\Canary as Debug; +use TheTempusProject\Bedrock\Functions\Check; +use TheTempusProject\Bedrock\Functions\Sanitize; +use TheTempusProject\Classes\DatabaseModel; +use TheTempusProject\TheTempusProject as App; +use TheTempusProject\Bedrock\Classes\Config; +use TheTempusProject\Hermes\Functions\Route as Routes; + +class MembershipCustomers extends DatabaseModel { + public static $stripe; + public $tableName = 'membership_customers'; + + public $databaseMatrix = [ + [ 'stripe_customer', 'varchar', '155' ], + [ 'local_user', 'varchar', '155' ], + // renews? + // does this renew periodically? + // renewal period? + // if periodic, how frequent? + // renewal type? + // automatic, manual review, paid + ]; + + public function __construct() { + parent::__construct(); + } + + public function findByUserID( $d ) { + $data = self::$db->get( $this->tableName, [ 'local_user', '=', $d ] ); + if ( ! $data->count() ) { + return false; + } + return $data->first(); + } + + public function findByCustomerID( $d ) { + $data = self::$db->get( $this->tableName, [ 'stripe_customer', '=', $d ] ); + if ( ! $data->count() ) { + return false; + } + return $data->first(); + } + + public function create( $user_id ) { + $data = self::$db->get( 'users', ['ID', '=', $user_id] ); + if ( !$data->count() ) { + Debug::warn( "customer cannot be created, user not found" ); + return false; + } + $user = $data->first(); + $user_email = $user->email; + $user_name = $user->name; + + $api_key = Config::getValue( 'memberships/stripeSecret' ); + if ( $api_key == 'sk_xxxxxxxxxxxxxxx' || empty($api_key) ) { + Debug::error( "MembershipCustomers:create No Stripe Key found" ); + return false; + } + self::$stripe = new \Stripe\StripeClient( $api_key ); + + $customer = self::$stripe->customers->create([ + 'name' => $user_name, + 'email' => $user_email, + ]); + + $fields = [ + 'stripe_customer' => $customer->id, + 'local_user' => $user_id, + ]; + + if ( !self::$db->insert( $this->tableName, $fields ) ) { + Debug::error( "Membership Customer: $data not added: $fields" ); + new customException( 'membershipCustomerCreate' ); + return false; + } + + return $customer; + } + + public function findOrCreate( $user_id ) { + $user = $this->findByUserID( $user_id ); + if ( ! empty( $user ) ) { + return $user->stripe_customer; + } + $user = $this->create( $user_id ); + if ( ! empty( $user ) ) { + return $user->id; + } + } +} \ No newline at end of file diff --git a/app/plugins/members/models/membership_invoices.php b/app/plugins/members/models/membership_invoices.php new file mode 100644 index 0000000..9412be8 --- /dev/null +++ b/app/plugins/members/models/membership_invoices.php @@ -0,0 +1,38 @@ + + * @link https://TheTempusProject.com + * @license https://opensource.org/licenses/MIT [MIT LICENSE] + */ +namespace TheTempusProject\Models; + +use TheTempusProject\Canary\Bin\Canary as Debug; +use TheTempusProject\Bedrock\Functions\Check; +use TheTempusProject\Bedrock\Functions\Sanitize; +use TheTempusProject\Classes\DatabaseModel; +use TheTempusProject\TheTempusProject as App; + +class MembershipInvoices extends DatabaseModel { + public static $stripe; + public $tableName = 'membership_invoices'; + + public $databaseMatrix = [ + [ 'name', 'varchar', '155' ], + // renews? + // does this renew periodically? + // renewal period? + // if periodic, how frequent? + // renewal type? + // automatic, manual review, paid + ]; + + public function __construct() { + parent::__construct(); + } +} \ No newline at end of file diff --git a/app/plugins/members/models/membership_products.php b/app/plugins/members/models/membership_products.php new file mode 100644 index 0000000..a4ab727 --- /dev/null +++ b/app/plugins/members/models/membership_products.php @@ -0,0 +1,222 @@ + + * @link https://TheTempusProject.com + * @license https://opensource.org/licenses/MIT [MIT LICENSE] + */ + +namespace TheTempusProject\Models; + +use TheTempusProject\Canary\Bin\Canary as Debug; +use TheTempusProject\Bedrock\Functions\Check; +use TheTempusProject\Bedrock\Functions\Sanitize; +use TheTempusProject\Classes\DatabaseModel; +use TheTempusProject\TheTempusProject as App; +use TheTempusProject\Bedrock\Classes\Config; + +class MembershipProducts extends DatabaseModel { + public static $stripe; + private static $loaded = false; + public $tableName = 'membership_products'; + public $databaseMatrix = [ + [ 'name', 'varchar', '128' ], + [ 'description', 'text', '' ], + [ 'monthly_price', 'int', '10' ], // must be int value greater than 99 IE $1.00 === 100, $4,399.22 === 439922 + [ 'yearly_price', 'int', '10' ], // must be int value greater than 99 IE $1.00 === 100, $4,399.22 === 439922 + [ 'stripe_product', 'varchar', '64' ], // not-required to create - generated by stripe after + [ 'stripe_price_monthly', 'varchar', '64' ], // not-required to create - generated by stripe after + [ 'stripe_price_yearly', 'varchar', '64' ], // not-required to create - generated by stripe after + ]; + + public function __construct() { + parent::__construct(); + if ( ! self::$loaded ) { + $api_key = Config::getValue( 'memberships/stripeSecret' ); + if ( $api_key == 'sk_xxxxxxxxxxxxxxx' || empty($api_key) ) { + Debug::error( "MembershipProducts:__construct No Stripe Key found" ); + } else { + self::$stripe = new \Stripe\StripeClient( $api_key ); + } + self::$loaded = true; + } + } + + public function create( $name, $description, $monthly_price, $yearly_price ) { + if ( empty( self::$stripe ) ) { + return false; + } + $stripe_product = $this->createStripeProduct( $name, $description ); + $stripe_prices = $this->createStripePrices( $stripe_product->id, $monthly_price, $yearly_price ); + + $fields = [ + 'name' => $name, + 'description' => $description, + 'monthly_price' => $monthly_price, + 'yearly_price' => $yearly_price, + 'stripe_product' => $stripe_product->id, + 'stripe_price_monthly' => $stripe_prices['monthly']->id, + 'stripe_price_yearly' => $stripe_prices['yearly']->id, + ]; + + if ( !self::$db->insert( $this->tableName, $fields ) ) { + Debug::error( "Membership Product: $data not updated: $fields" ); + new customException( 'membershipProductCreate' ); + return false; + } + + return true; + } + + public function createStripeProduct( $name, $description ) { + if ( empty( self::$stripe ) ) { + return false; + } + $product = self::$stripe->products->create([ + 'name' => $name, + 'description' => $description, + ]); + return $product; + } + + public function createStripePrices( $product, $monthly_price, $yearly_price ) { + $out = []; + $out['monthly'] = $this->createStripeMonthlyPrice( $product, $monthly_price ); + $out['yearly'] = $this->createStripeYearlyPrice( $product, $yearly_price ); + return $out; + } + + public function createStripeMonthlyPrice( $product, $monthly_price ) { + if ( empty( self::$stripe ) ) { + return false; + } + return self::$stripe->prices->create([ + 'currency' => 'usd', + 'unit_amount' => $monthly_price, + 'recurring' => ['interval' => 'month'], + 'product' => $product, + 'lookup_key' => 'membership-monthly', + ]); + } + + public function createStripeYearlyPrice( $product, $yearly_price ) { + if ( empty( self::$stripe ) ) { + return false; + } + return self::$stripe->prices->create([ + 'currency' => 'usd', + 'unit_amount' => $yearly_price, + 'recurring' => ['interval' => 'year'], + 'product' => $product, + 'lookup_key' => 'membership-yearly', + ]); + } + + public function updateProduct( $id, $name, $description, $monthly_price, $yearly_price ) { + $product = $this->findById( $id ); + if ( $product === false ) { + return false; + } + if ( $product->monthly_price != $monthly_price ) { + $new_monthly = $this->updateStripeMonthlyPrice( $product->stripe_price_monthly, $monthly_price ); + } + if ( $product->yearly_price != $yearly_price ) { + $new_yearly = $this->updateStripeYearlyPrice( $product->stripe_price_yearly, $yearly_price ); + } + if ( ( $product->name != $name ) || ( $product->description != $description ) ) { + $this->updateStripeProduct( $product->stripe_product, $name, $description ); + } + + $fields = [ + 'name' => $name, + 'description' => $description, + 'monthly_price' => $monthly_price, + 'yearly_price' => $yearly_price, + 'stripe_price_monthly' => $new_monthly->id, + 'stripe_price_yearly' => $new_yearly->id, + ]; + if ( !self::$db->update( $this->tableName, $id, $fields ) ) { + new CustomException( 'membershipProductUpdate' ); + Debug::error( "membership Product: $id not updated: $fields" ); + + return false; + } + self::$log->admin( "Updated membership Product: $id" ); + return true; + } + + public function updateStripeProduct( $id, $name, $description ) { + if ( empty( self::$stripe ) ) { + return false; + } + $product = self::$stripe->products->update( + $id, + [ + 'name' => $name, + 'description' => $description, + ] + ); + return $product; + } + + public function updateStripeYearlyPrice( $product, $yearly_price ) { + $stripe->prices->update( + $yearly_price, + ['lookup_key' => ""] + ); + return $this->createStripeYearlyPrice( $product, $yearly_price ); + } + + public function updateStripeMonthlyPrice( $product, $monthly_price ) { + $stripe->prices->update( + $monthly_price, + ['lookup_key' => ""] + ); + return $this->createStripeMonthlyPrice( $product, $monthly_price ); + } + + public function filter( $postArray, $params = [] ) { + foreach ( $postArray as $instance ) { + if ( !is_object( $instance ) ) { + $instance = $postArray; + $end = true; + } + + $instance->prettyPriceMonthly = '$' . number_format( $instance->monthly_price / 100, 2 ); // Outputs: $99.49 + $instance->prettyPriceYearly = '$' . number_format( $instance->yearly_price / 100, 2 ); // Outputs: $99.49 + $instance->prettySavings = '$' . number_format( + ( $instance->yearly_price - ( $instance->monthly_price * 12 ) ) / 100, + 2 + ); // Outputs: $99.49 + + $out[] = $instance; + if ( !empty( $end ) ) { + $out = $out[0]; + break; + } + } + return $out; + } + + public function findByPriceID( $d ) { + $data = self::$db->get( $this->tableName, [ 'stripe_price_monthly', '=', $d, 'OR', 'stripe_price_yearly', '=', $d ] ); + if ( ! $data->count() ) { + return false; + } + return $this->filter( $data->first() ); + } + + public function mainProduct() { + $data = self::$db->get( $this->tableName, '*' ); + if ( ! $data->count() ) { + return false; + } + + return $this->filter( $data->first() ); + } +} \ No newline at end of file diff --git a/app/plugins/members/models/memberships.php b/app/plugins/members/models/memberships.php new file mode 100644 index 0000000..43e5916 --- /dev/null +++ b/app/plugins/members/models/memberships.php @@ -0,0 +1,174 @@ + + * @link https://TheTempusProject.com + * @license https://opensource.org/licenses/MIT [MIT LICENSE] + */ +namespace TheTempusProject\Models; + +use TheTempusProject\Canary\Bin\Canary as Debug; +use TheTempusProject\Bedrock\Functions\Check; +use TheTempusProject\Bedrock\Functions\Sanitize; +use TheTempusProject\Classes\DatabaseModel; +use TheTempusProject\TheTempusProject as App; +use TheTempusProject\Models\MembershipProducts; +use TheTempusProject\Bedrock\Classes\Config; +use TheTempusProject\Canary\Classes\CustomException; + +class Memberships extends DatabaseModel { + public static $stripe; + public static $products; + private static $loaded = false; + public $tableName = 'membership_records'; + public $databaseMatrix = [ + [ 'stripe_customer', 'varchar', '64' ], + [ 'stripe_subscription', 'varchar', '64' ], + [ 'subscription_price_id', 'varchar', '64' ], + [ 'current_period_end', 'int', '10' ], + [ 'current_period_start', 'int', '10' ], + [ 'status', 'varchar', '64' ], + [ 'local_user_id', 'int', '10' ], + [ 'billing_frequency', 'varchar', '16' ], + ]; + + public function __construct() { + parent::__construct(); + if ( ! self::$loaded ) { + self::$products = new MembershipProducts; + $api_key = Config::getValue( 'memberships/stripeSecret' ); + if ( $api_key == 'sk_xxxxxxxxxxxxxxx' || empty($api_key) ) { + Debug::error( "Memberships:__construct No Stripe Key found" ); + } else { + self::$stripe = new \Stripe\StripeClient( $api_key ); + } + self::$loaded = true; + } + } + + public function filter( $postArray, $params = [] ) { + foreach ( $postArray as $instance ) { + if ( !is_object( $instance ) ) { + $instance = $postArray; + $end = true; + } + $instance->name = self::$user->getUsername( $instance->local_user_id ); + $priceData = self::$products->findByPriceID( $instance->subscription_price_id ); + if ( $priceData ) { + if ( $priceData->stripe_price_monthly == $instance->subscription_price_id ) { + $price = $priceData->monthly_price; + } else { + $price = $priceData->yearly_price; + } + $instance->productName = $priceData->name; + $instance->prettyPrice = '$' . number_format( $price / 100, 2 ); // Outputs: $99.49 + } else { + $instance->prettyPrice = "unknown"; + $instance->productName = "unknown"; + } + $out[] = $instance; + if ( !empty( $end ) ) { + $out = $out[0]; + break; + } + } + return $out; + } + + public function getUserSubs( $limit = null ) { + $whereClause = ['local_user_id', '=', App::$activeUser->ID ]; + if ( empty( $limit ) ) { + $postData = self::$db->get( $this->tableName, $whereClause ); + } else { + $postData = self::$db->get( $this->tableName, $whereClause, 'ID', 'DESC', [0, $limit] ); + } + if ( !$postData->count() ) { + Debug::info( 'No user subs found.' ); + return false; + } + return $this->filter( $postData->results() ); + } + + public function findByUserID( $d ) { + $data = self::$db->get( $this->tableName, [ 'local_user_id', '=', $d ] ); + if ( ! $data->count() ) { + return false; + } + return $data->first(); + } + + public function findActiveByUserID( $d ) { + $data = self::$db->get( $this->tableName, [ 'local_user_id', '=', $d, 'AND', 'status', '=', 'active' ] ); + if ( ! $data->count() ) { + return false; + } + return $data->first(); + } + + public function cancel( $id ) { + $data = self::$db->get( $this->tableName, [ 'ID', '=', $id, 'AND', 'local_user_id', '=', App::$activeUser->ID, 'AND', 'status', '=', 'active' ] ); + if ( ! $data->count() ) { + return false; + } + $membershipID = $data->first()->stripe_subscription; + $out = false; + try { + $out = self::$stripe->subscriptions->cancel($membershipID, []); + } catch(\Exception $e) { + Debug::error( 'Exception' ); + Debug::v( $e ); + } + return $out; + } + + public function findBySubscriptionID( $d ) { + $data = self::$db->get( $this->tableName, [ 'stripe_subscription', '=', $d ] ); + if ( ! $data->count() ) { + return false; + } + return $data->first(); + } + + public function create( $customer, $subscription, $price, $start, $end, $status, $user_id, $frequency ) { + $fields = [ + 'stripe_customer' => $customer, + 'stripe_subscription' => $subscription, + 'subscription_price_id' => $price, + 'current_period_end' => $end, + 'current_period_start' => $start, + 'status' => $status, + 'local_user_id' => $user_id, + 'billing_frequency' => $frequency, + ]; + + if ( !self::$db->insert( $this->tableName, $fields ) ) { + Debug::error( "Memberships: not created: $fields" ); + new CustomException( 'membershipsCreate' ); + return false; + } + + return true; + } + + public function update( $id, $start, $end, $status ) { + $fields = [ + 'current_period_end' => $end, + 'current_period_start' => $start, + 'status' => $status, + ]; + + if ( !self::$db->update( $this->tableName, $id, $fields ) ) { + Debug::error( "Memberships: not updated: " ); + Debug::v( $fields ); + new CustomException( 'membershipsUpdate' ); + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/app/plugins/members/plugin.php b/app/plugins/members/plugin.php new file mode 100644 index 0000000..b347f47 --- /dev/null +++ b/app/plugins/members/plugin.php @@ -0,0 +1,263 @@ + + * @link https://TheTempusProject.com + * @license https://opensource.org/licenses/MIT [MIT LICENSE] + */ +namespace TheTempusProject\Plugins; + +use TheTempusProject\TheTempusProject as App; +use TheTempusProject\Classes\Plugin; +use Stripe\StripeClient; +use TheTempusProject\Bedrock\Classes\Config; +use TheTempusProject\Hermes\Functions\Route as Routes; +use TheTempusProject\Models\Memberships; +use TheTempusProject\Models\MembershipProducts as Products; +use TheTempusProject\Canary\Bin\Canary as Debug; + +class Members extends Plugin { + public static $stripe; + public static $memberships; + private static $loaded = false; + public $pluginName = 'TP Membership'; + public $configName = 'memberships'; + public $pluginAuthor = 'JoeyK'; + public $pluginWebsite = 'https://TheTempusProject.com'; + public $modelVersion = '1.0'; + public $pluginVersion = '3.0'; + public $pluginDescription = 'A simple plugin which adds a site wide membership system.'; + public $permissionMatrix = [ + 'memberAccess' => [ + 'pretty' => 'Access Member Areas', + 'default' => false, + ], + 'controlMemberships' => [ + 'pretty' => 'User can Access and Control user memberships.', + 'default' => false, + ], + ]; + public $admin_links = [ + [ + 'text' => ' Memberships', + 'url' => [ + [ + 'text' => ' Products', + 'url' => '{ROOT_URL}admin/products', + ], + [ + 'text' => ' Subscriptions', + 'url' => '{ROOT_URL}admin/records', + ], + [ + 'text' => ' Scripts', + 'url' => '{ROOT_URL}admin/members', + ], + ], + ], + ]; + public $main_links = [ + [ + 'text' => 'My Membership', + 'url' => '{ROOT_URL}member/index', + 'filter' => 'member', + ], + [ + 'text' => 'Subscribe', + 'url' => '{ROOT_URL}member/join', + 'filter' => 'nonmember', + ], + [ + 'text' => 'Upgrade', + 'url' => '{ROOT_URL}member/upgrade', + 'filter' => 'upgrade', + ], + ]; + public $resourceMatrix = [ + 'groups' => [ + [ + 'name' => 'Member', + 'permissions' => '{"adminAccess":false}', + ] + ], + ]; + public $configMatrix = [ + 'stripePublishable' => [ + 'type' => 'text', + 'pretty' => 'Stripe Publishable key', + 'default' => 'pk_xxxxxxxxxxxxxxx', + ], + 'stripeSecret' => [ + 'type' => 'text', + 'pretty' => 'Stripe Secret key', + 'default' => 'sk_xxxxxxxxxxxxxxx', + ], + ]; + public static $webhookEvents = [ + 'customer.subscription.created', + 'customer.subscription.updated', + 'customer.subscription.deleted', + 'customer.updated', + 'customer.deleted', + 'payment_method.automatically_updated', + 'invoice.payment_action_required', + 'invoice.payment_succeeded', + 'checkout.session.completed', + ]; + public static $userLinks = [ + "url" => "{ROOT_URL}member/manage", + "name" => "Subscriptions" + ]; + + public function __construct( $load = false ) { + if ( ! self::$loaded && $load ) { + if ( App::$isLoggedIn ) { + App::$isMember = $this->groupHasMemberAccess( App::$activeGroup ); + if ( empty( App::$isMember ) ) { + App::$isMember = $this->userHasActiveMembership( App::$activeUser->ID ); + } + } + $this->filters[] = [ + 'name' => 'member', + 'find' => '#{MEMBER}(.*?){/MEMBER}#is', + 'replace' => ( App::$isMember ? '$1' : '' ), + 'enabled' => true, + ]; + $this->filters[] = [ + 'name' => 'nonmember', + 'find' => '#{NONMEMBER}(.*?){/NONMEMBER}#is', + 'replace' => ( App::$isLoggedIn && App::$isMember == false ? '$1' : '' ), + 'enabled' => true, + ]; + $this->filters[] = [ + 'name' => 'upgrade', + 'find' => '#{UPGRADE}(.*?){/UPGRADE}#is', + 'replace' => ( App::$isLoggedIn && ( App::$isMember === 'monthly' ) ? '$1' : '' ), + 'enabled' => true, + ]; + } + + parent::__construct( $load ); + + if ( $this->checkEnabled() && App::$isLoggedIn ) { + if ( ! self::$loaded && $load ) { + App::$userCPlinks[] = (object) self::$userLinks; + App::$topNavRightDropdown .= '
  • Subscriptions
  • '; + } + } + + if ( ! self::$loaded && $load ) { + self::$loaded = true; + } + + $api_key = Config::getValue( 'memberships/stripeSecret' ); + if ( $api_key == 'sk_xxxxxxxxxxxxxxx' || empty($api_key) ) { + self::$stripe = false; + } else { + self::$stripe = new StripeClient( $api_key ); + } + } + + public function groupHasMemberAccess( $activeGroup ) { + if ( $activeGroup->memberAccess == true ) { + return true; + } + return false; + } + + public function userHasActiveMembership( $user_id ) { + $memberships = new Memberships; + $membership = $memberships->findActiveByUserID( $user_id ); + if ( empty( $membership ) ) { + return false; + } + + $products = new Products; + $product = $products->findByPriceID( $membership->subscription_price_id ); + if ( empty( $product ) ) { + Debug::error('Active membership on non-existent product'); + return false; + } + if ( $product->stripe_price_monthly == $membership->subscription_price_id ) { + return 'monthly'; + } + return 'yearly'; + } + + public static function webhookSetup() { + $root = Routes::getAddress(); + $response = self::$stripe->webhookEndpoints->create([ + 'enabled_events' => self::$webhookEvents, + 'url' => $root . 'api/stripe/webhook', + ]); + return $response; + } + + public static function webhookList() { + $response = self::$stripe->webhookEndpoints->all(['limit' => 25]); + return $response; + } + + public static function webhookRemove( $id ) { + $response = self::$stripe->webhookEndpoints->delete( $id, []); + return $response; + } + + public static function orphanAbandon( $id ) { + $response = self::$stripe->prices->update( + $id, + ['lookup_key' => ""] + ); + return $response; + } + + public static function findOrphans() { + $orphans = []; + // any prices with keys not represented in our local db will show as an orphan + + $result = self::$stripe->prices->search([ + 'query' => 'lookup_key:"membership-monthly"', + ]); + + $products = new Products; + if ( ! empty( $result->data ) ) { + $product = $products->findByPriceID( $result->data[0]->id ); + + if ( empty( $product ) ) { + $data = $result->data[0]; + + $child = new \stdClass; + $child->price_id = $data->id; + $child->product = $data->product; + $child->lookup_key = $data->lookup_key; + $child->amount = $data->unit_amount; + $orphans[] = $child; + } + } + + $result = self::$stripe->prices->search([ + 'query' => 'lookup_key:"membership-yearly"', + ]); + if ( ! empty( $result->data ) ) { + $product = $products->findByPriceID( $result->data[0]->id ); + + if ( empty( $product ) ) { + $data = $result->data[0]; + + $child = new \stdClass; + $child->price_id = $data->id; + $child->product = $data->product; + $child->lookup_key = $data->lookup_key; + $child->amount = $data->unit_amount; + $orphans[] = $child; + } + } + + return $orphans; + } +} diff --git a/app/plugins/members/views/admin/memberships/list.html b/app/plugins/members/views/admin/memberships/list.html new file mode 100644 index 0000000..3c51c02 --- /dev/null +++ b/app/plugins/members/views/admin/memberships/list.html @@ -0,0 +1,47 @@ +
    + Memberships +
    + {ADMIN_BREADCRUMBS} +
    + + + + + + + + + + + + + + + {LOOP} + + + + + + + + + + + {/LOOP} + {ALT} + + + + {/ALT} + +
    NamestatusPriceStartEnd + +
    {name}{status}{prettyPrice}{DTC}{current_period_start}{/DTC}{DTC}{current_period_end}{/DTC} + +
    + No results to show. +
    + +
    +
    \ No newline at end of file diff --git a/app/plugins/members/views/admin/memberships/view.html b/app/plugins/members/views/admin/memberships/view.html new file mode 100644 index 0000000..22cdb90 --- /dev/null +++ b/app/plugins/members/views/admin/memberships/view.html @@ -0,0 +1,76 @@ +
    +
    +
    + {ADMIN_BREADCRUMBS} +
    + +
    +

    {name}

    +
    + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    User{name}
    Status{status}
    Billing Frequency{billing_frequency}
    Started:{DTC}{current_period_end}{/DTC}
    Ended:{DTC}{current_period_start}{/DTC}
    Stripe Customer + + +
    Stripe Subscription + + +
    Stripe Subscription Price + + +
    +
    +
    + + + +
    +
    +
    +
    \ No newline at end of file diff --git a/app/plugins/members/views/admin/orphans.html b/app/plugins/members/views/admin/orphans.html new file mode 100644 index 0000000..e3246af --- /dev/null +++ b/app/plugins/members/views/admin/orphans.html @@ -0,0 +1,35 @@ +
    + Stripe Webhook Generation +
    + {ADMIN_BREADCRUMBS} +

    Orphans are stripe prices that have unique lookup_keys used by the membership system, but have no products currently saved.

    + + + + + + + + + + + + {LOOP} + + + + + + + + {/LOOP} + {ALT} + + + + {/ALT} + +
    price idamountlookup keyurl
    {price_id}{amount}{lookup_key}
    + No results to show. +
    +
    \ No newline at end of file diff --git a/app/plugins/members/views/admin/products/create.html b/app/plugins/members/views/admin/products/create.html new file mode 100644 index 0000000..b26ec45 --- /dev/null +++ b/app/plugins/members/views/admin/products/create.html @@ -0,0 +1,51 @@ +
    + Create Membership Product +
    + {ADMIN_BREADCRUMBS} +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + + Integer required: $4.99 : 499 +
    +
    + + +
    + +
    + + Integer required: $4.99 : 499 +
    +
    + + + +
    + +
    + + Max: 2000 characters +
    +
    + + + +
    + + +
    + +
    +
    +
    \ No newline at end of file diff --git a/app/plugins/members/views/admin/products/edit.html b/app/plugins/members/views/admin/products/edit.html new file mode 100644 index 0000000..484bc41 --- /dev/null +++ b/app/plugins/members/views/admin/products/edit.html @@ -0,0 +1,51 @@ +
    + Create Membership Product +
    + {ADMIN_BREADCRUMBS} +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + + Integer required: $4.99 : 499 +
    +
    + + +
    + +
    + + Integer required: $4.99 : 499 +
    +
    + + + +
    + +
    + + Max: 2000 characters +
    +
    + + + +
    + + +
    + +
    +
    +
    \ No newline at end of file diff --git a/app/plugins/members/views/admin/products/list.html b/app/plugins/members/views/admin/products/list.html new file mode 100644 index 0000000..2b44f4d --- /dev/null +++ b/app/plugins/members/views/admin/products/list.html @@ -0,0 +1,46 @@ +
    + Membership Products +
    + {ADMIN_BREADCRUMBS} +
    + + + + + + + + + + + + + + {LOOP} + + + + + + + + + + {/LOOP} + {ALT} + + + + {/ALT} + +
    NameMonthly PriceYearly Price + +
    {name}{monthly_price}{yearly_price} + +
    + No results to show. +
    + Create + +
    +
    \ No newline at end of file diff --git a/app/plugins/members/views/admin/products/view.html b/app/plugins/members/views/admin/products/view.html new file mode 100644 index 0000000..f33e987 --- /dev/null +++ b/app/plugins/members/views/admin/products/view.html @@ -0,0 +1,72 @@ +
    +
    +
    + {ADMIN_BREADCRUMBS} +
    + +
    +

    {name}

    +
    + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Name{name}
    Monthly Price{monthly_price}
    Yearly Price{yearly_price}
    Stripe Product + + +
    Stripe Price Yearly + + +
    Stripe Price Monthly + + +
    Description{description}
    +
    +
    + + + +
    +
    +
    +
    \ No newline at end of file diff --git a/app/plugins/members/views/admin/scripts.html b/app/plugins/members/views/admin/scripts.html new file mode 100644 index 0000000..b170924 --- /dev/null +++ b/app/plugins/members/views/admin/scripts.html @@ -0,0 +1,13 @@ +
    + Membership Scripts +
    + {ADMIN_BREADCRUMBS} + +
    \ No newline at end of file diff --git a/app/plugins/members/views/admin/webhooks.html b/app/plugins/members/views/admin/webhooks.html new file mode 100644 index 0000000..0a8b5c1 --- /dev/null +++ b/app/plugins/members/views/admin/webhooks.html @@ -0,0 +1,43 @@ +
    + Stripe Webhook Generation +
    + {ADMIN_BREADCRUMBS} + + + + + + + + + + + + {LOOP} + + + + + + + + {/LOOP} + {ALT} + + + + {/ALT} + +
    idstatusenabled_eventsurl
    {id}{status}{enabled_events}{url}
    + No results to show. +
    +
    +
    WARNING: Regenerating existing webhooks makes joey sad, don't do iit!
    +

    The new webhooks will be generated using the base url: {urltouse}

    +
    +
    + +
    +
    +
    +
    \ No newline at end of file diff --git a/app/plugins/members/views/cancel.html b/app/plugins/members/views/cancel.html new file mode 100644 index 0000000..af476c3 --- /dev/null +++ b/app/plugins/members/views/cancel.html @@ -0,0 +1,20 @@ +
    +

    Are You Sure You Want to Cancel?

    +
    +
    +

    + Cancelling your subscription means you'll miss out on exclusive features, updates, and benefits. +

    +

    + Consider staying to continue enjoying the full experience of our service. +

    + +
    +
    \ No newline at end of file diff --git a/app/plugins/members/views/join.html b/app/plugins/members/views/join.html new file mode 100644 index 0000000..d124c21 --- /dev/null +++ b/app/plugins/members/views/join.html @@ -0,0 +1,130 @@ + +
    +

    Compare plans

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FreeMonthlyYearly
    Add and Manage Bookmarks
    Extensions for all major browsers
    Access from any device
    Share bookmarks and folders
    Import/Export Features
    Customizable Dashboards / Pages
    Request/Influence Development
    Early Access
    Cheaper
    +
    + +
    + + +
    +
    +
    +
    +
    +

    Free

    +
    +
    +

    $0/mo

    +
      +
    • Add / Manage your bookmarks
    • +
    • Extensions for all major browsers
    • +
    • Access from any device
    • +
    • Share access with anyone
    • +
    +
    +
    +
    +
    +
    +
    +

    Monthly

    +
    +
    +

    {prettyPriceMonthly}/month

    +
      +
    • Import/Export Features
    • +
    • Integration with TempusTools App (WIP)
    • +
    • Customizable Dashboards / Pages
    • +
    • Direct control of Feature Development
    • +
    • Early Access to new features
    • +
    + + Get started + +
    +
    +
    +
    +
    +
    +

    Yearly

    +
    +
    +

    {prettyPriceYearly}/year

    +
      +
    • Its cheaper if you like the product
    • +
    + + Get started + +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/app/plugins/members/views/manage.html b/app/plugins/members/views/manage.html new file mode 100644 index 0000000..bee1148 --- /dev/null +++ b/app/plugins/members/views/manage.html @@ -0,0 +1,52 @@ +
    +

    Manage Memberships

    +
    +
    +
    + + + + + + + + + + + + + + {LOOP} + + + + + + + + + + {/LOOP} + {ALT} + + + + {/ALT} + +
    NameStatusPriceStartEnd
    {productName}{status}{prettyPrice}{DTC=date}{current_period_start}{/DTC}{DTC=date}{current_period_end}{/DTC} + + + + + + + +
    + No results to show. +
    + + Manage Payment Method + +
    +
    +
    diff --git a/app/plugins/members/views/members.html b/app/plugins/members/views/members.html new file mode 100644 index 0000000..96636da --- /dev/null +++ b/app/plugins/members/views/members.html @@ -0,0 +1,18 @@ +
    +

    Membership Benefits

    +

    + First, let me say thank you for choosing to become a member! There are several great benefits exclusively for members. +

    +

    + In addition to extra features for bookmark management like dashboards and import/export, you gain access to influence development. Suggestions gives users a direct way to make suggestions to me personally. By default, suggestions are not public, but I can comment on them to let you know what I think. + All respectful and reasonable suggestions go up for the entire community to see and comment on. There iis going to be a loyalty points system in the future to allow you to accrue points and use the points to vote for suggestions and features they like. But that's for another time. +

    +

    + Right now, this entire system was built and managed by myself. I have used my own version of this for years, but translating it to a publicly available product is not a 1-to-1 job. There may be bugs or issues encountered while you use the product. I can't guarantee a fix for every need in every case immediately, but I do actively keep track of bugs and work hard to ensure everyone has a great experience using the app. +

    +
    + {loggedin}Report a Bug{/loggedin} + Manage Membership + Contact Us +
    +
    \ No newline at end of file diff --git a/app/plugins/members/views/pause.html b/app/plugins/members/views/pause.html new file mode 100644 index 0000000..bd09161 --- /dev/null +++ b/app/plugins/members/views/pause.html @@ -0,0 +1,20 @@ +
    +

    Are You Sure You Want to Pause?

    +
    +
    +

    + Pausing your subscription means you'll miss out on exclusive features, updates, and benefits. +

    +

    + Consider staying to continue enjoying the full experience of our service. +

    +
    + +
    + Go Back +
    +
    +
    +
    \ No newline at end of file diff --git a/app/plugins/members/views/paymentcanceled.html b/app/plugins/members/views/paymentcanceled.html new file mode 100644 index 0000000..2dc6a47 --- /dev/null +++ b/app/plugins/members/views/paymentcanceled.html @@ -0,0 +1,14 @@ +
    +

    Almost there!

    +

    + Take your time, its not a sprint, its a marathon. + Nno-one wants to checkout too fast, its embarrassing. +

    +

    + If you think this is a tool that you could really use and can't afford it, use the contact form below and tell me why you need it. +

    +
    + {loggedin}Report a Bug{/loggedin} + Contact Us +
    +
    \ No newline at end of file diff --git a/app/plugins/members/views/paymentcomplete.html b/app/plugins/members/views/paymentcomplete.html new file mode 100644 index 0000000..1af195d --- /dev/null +++ b/app/plugins/members/views/paymentcomplete.html @@ -0,0 +1,14 @@ +
    +

    Thanks for joining!

    +

    + Its people like you who keep the pixels on around here. + Its people like me who tell people like you that unfortunately, it could take up to an hour for your membership to activate. +

    +

    + With that said, its usually instant, and if its taking too long, just reach out to us via the contact form. +

    +
    + {loggedin}Report a Bug{/loggedin} + Contact Us +
    +
    \ No newline at end of file diff --git a/app/plugins/members/views/register.html b/app/plugins/members/views/register.html new file mode 100644 index 0000000..9bdd1cf --- /dev/null +++ b/app/plugins/members/views/register.html @@ -0,0 +1,69 @@ +
    +

    Create an Account

    +

    After registration is complete, you will be redirected to Stripe to handle payment.

    +

    + You have selected the {planName} plan at {prettyPrice}. +

    +
    + +
    + +
    + +
    +
    + + +
    + +
    + +
    +
    + + +
    + +
    + +
    +
    + + +
    + +
    + +
    +
    + + +
    + +
    + +
    +
    + + +
    +
    + + +
    +
    + {TERMS} +
    +
    + + + + + +
    + +
    +
    +
    diff --git a/app/plugins/members/views/upgrade.html b/app/plugins/members/views/upgrade.html new file mode 100644 index 0000000..8e5d4a7 --- /dev/null +++ b/app/plugins/members/views/upgrade.html @@ -0,0 +1,24 @@ +
    +
    +

    Upgrade to a Yearly Plan

    +

    + Save more and enjoy uninterrupted access to all features with our yearly plan! +

    +

    + Upgrading now means you'll save nearly $40 a year compared to the monthly plan. + Stay committed and make the most of our service. +

    + +
    +
    \ No newline at end of file diff --git a/app/plugins/members/views/upgradeCompleted.html b/app/plugins/members/views/upgradeCompleted.html new file mode 100644 index 0000000..7bc7811 --- /dev/null +++ b/app/plugins/members/views/upgradeCompleted.html @@ -0,0 +1,13 @@ +
    +

    Thanks for the vote of confidence!

    +

    + While you were more profitable as a monthly user, I do appreciate the vote of confidence it takes to commit to 4x the cost at once. +

    +

    + There really is no more benefit than you saving money, so enjoy the features you have already come to know and enjoy. +

    +
    + {loggedin}Report a Bug{/loggedin} + Contact Us +
    +
    \ No newline at end of file diff --git a/app/plugins/reviews/controllers/admin/reviews.php b/app/plugins/reviews/controllers/admin/reviews.php new file mode 100644 index 0000000..bfac95f --- /dev/null +++ b/app/plugins/reviews/controllers/admin/reviews.php @@ -0,0 +1,140 @@ + + * @link https://TheTempusProject.com + * @license https://opensource.org/licenses/MIT [MIT LICENSE] + */ +namespace TheTempusProject\Controllers\Admin; + +use TheTempusProject\Bedrock\Functions\Input; +use TheTempusProject\Houdini\Classes\Issues; +use TheTempusProject\Houdini\Classes\Views; +use TheTempusProject\Houdini\Classes\Navigation; +use TheTempusProject\Houdini\Classes\Components; +use TheTempusProject\Classes\AdminController; +use TheTempusProject\Models\Review; +use TheTempusProject\Models\ReviewCategory; +use TheTempusProject\Classes\Forms; + +class Reviews extends AdminController { + protected static $reviews; + protected static $categories; + + public function __construct() { + parent::__construct(); + self::$title = 'Admin - Reviews'; + self::$reviews = new Review; + self::$categories = new ReviewCategory; + $view = Navigation::activePageSelect( 'nav.admin', '/admin/reviews' ); + Components::set( 'ADMINNAV', $view ); + } + + public function index( $data = null ) { + $reviews = Views::simpleView( 'reviews.admin.reviewList', self::$reviews->list() ); + $categories = Views::simpleView( 'reviews.admin.categoryList', self::$categories->list() ); + Components::set( 'reviews', $reviews ); + Components::set( 'reviewCategories', $categories ); + Views::view( 'reviews.admin.dashboard' ); + } + + public function categoryView( $id = null ) { + $category = self::$categories->findById( $id ); + if ( ! $category ) { + Issues::add( 'error', 'Unknown Category.' ); + return $this->index(); + } + Views::view( 'reviews.admin.category', $category ); + } + + public function categoryCreate( ) { + if ( ! Input::exists( 'submit' ) ) { + return Views::view( 'reviews.admin.categoryCreate' ); + } + if ( ! Forms::check( 'categoryCreate' ) ) { + Issues::add( 'error', [ 'There was an error with your request.' => Check::userErrors() ] ); + return Views::view( 'reviews.admin.categoryCreate' ); + } + $result = self::$categories->create( + Input::post( 'name' ), + Input::post( 'slug' ), + ); + if ( $result ) { + Issues::add( 'success', 'Your review category has been created.' ); + return $this->index(); + } + Issues::add( 'error', [ 'There was an unknown error submitting your data.' => Check::userErrors() ] ); + Views::view( 'reviews.admin.categoryCreate' ); + } + + public function categoryEdit( $id = null ) { + $category = self::$categories->findById( $id ); + if ( ! $category ) { + Issues::add( 'error', 'Unknown Category.' ); + return $this->index(); + } + if ( ! Input::exists( 'submit' ) ) { + return Views::view( 'reviews.admin.categoryEdit', $category ); + } + if ( ! Forms::check( 'categoryEdit' ) ) { + Issues::add( 'error', [ 'There was an error with your request.' => Check::userErrors() ] ); + return Views::view( 'reviews.admin.categoryEdit', $category ); + } + $result = self::$categories->update( + $id, + Input::post( 'name' ), + Input::post( 'slug' ), + ); + if ( $result ) { + Issues::add( 'success', 'Your review category has been updated.' ); + return $this->index(); + } + Issues::add( 'error', [ 'There was an unknown error submitting your data.' => Check::userErrors() ] ); + Views::view( 'reviews.admin.categoryEdit', $category ); + } + + public function categoryDelete( $id = null ) { + if ( self::$categories->delete( $id ) ) { + Issues::add( 'success', 'review category deleted' ); + } else { + Issues::add( 'error', 'There was an error with your request.' ); + } + $this->index(); + } + + public function reviewView( $id = null ) { + Views::view( 'reviews.admin.review', self::$reviews->findById( $id ) ); + } + + public function reviewApprove( $id = null ) { + if ( self::$reviews->approve( $id ) ) { + Issues::add( 'success', 'review approved' ); + } else { + Issues::add( 'error', 'There was an error with your request.' ); + } + $this->index(); + } + + public function reviewHide( $id = null ) { + if ( self::$reviews->hide( $id ) ) { + Issues::add( 'success', 'review hidden' ); + } else { + Issues::add( 'error', 'There was an error with your request.' ); + } + $this->index(); + } + + public function reviewDelete( $id = null ) { + if ( self::$reviews->delete( $id ) ) { + Issues::add( 'success', 'review deleted' ); + } else { + Issues::add( 'error', 'There was an error with your request.' ); + } + $this->index(); + } +} diff --git a/app/plugins/reviews/controllers/reviews.php b/app/plugins/reviews/controllers/reviews.php new file mode 100644 index 0000000..dd14b16 --- /dev/null +++ b/app/plugins/reviews/controllers/reviews.php @@ -0,0 +1,156 @@ + + * @link https://TheTempusProject.com + * @license https://opensource.org/licenses/MIT [MIT LICENSE] + */ +namespace TheTempusProject\Controllers; + +use TheTempusProject\Hermes\Functions\Redirect; +use TheTempusProject\Bedrock\Functions\Check; +use TheTempusProject\Bedrock\Functions\Input; +use TheTempusProject\Bedrock\Functions\Session; +use TheTempusProject\Houdini\Classes\Issues; +use TheTempusProject\Houdini\Classes\Views; +use TheTempusProject\Classes\Controller; +use TheTempusProject\Classes\Forms; +use TheTempusProject\Models\Review; +use TheTempusProject\TheTempusProject as App; +use TheTempusProject\Houdini\Classes\Components; +use TheTempusProject\Houdini\Classes\Template; +use TheTempusProject\Models\ReviewCategory; +use TheTempusProject\Houdini\Classes\Forms as HoudiniForms; + +class Reviews extends Controller { + protected static $reviews; + protected static $categories; + + public function __construct() { + self::$title = 'Reviews - {SITENAME}'; + self::$pageDescription = 'This page allows you to add a new product review or update your existing reviews.'; + + if ( ! App::$isLoggedIn ) { + Session::flash( 'notice', 'You must be logged in to review products.' ); + return Redirect::home(); + } + + parent::__construct(); + self::$reviews = new Review; + self::$categories = new ReviewCategory; + Components::append( 'TEMPLATE_JS_INCLUDES', Template::parse('' ) ); + Components::append( 'TEMPLATE_CSS_INCLUDES', Template::parse('') ); + } + + public function index() { + Views::view( 'reviews.list', self::$reviews->byUser() ); + } + + public function review( $slug = null ) { + $category = self::$categories->findBySlug( $slug ); + if ( ! $category ) { + $categories = self::$categories->simpleUnreviewed(); + if ( empty( $categories ) ) { + Session::flash( 'notice', 'There are no additional products to review at this time.' ); + Redirect::to( 'reviews/index' ); + } + $selectedCategory = '0'; + $reviewCategorySelect = HoudiniForms::getSelectHtml( + 'review_category_id', + $categories, + $selectedCategory, + ); + Components::set( 'reviewCategorySelect', $reviewCategorySelect ); + } else { + $selectedCategory = $category->ID; + Components::set( + 'reviewCategorySelect', + '' + ); + } + if ( ! Input::exists('submit') ) { + return Views::view( 'reviews.create' ); + } + if ( ! Forms::check( 'createReview' ) ) { + Issues::add( 'error', [ 'There was an error with your review.' => Check::userErrors() ] ); + return Views::view( 'reviews.create' ); + } + $result = self::$reviews->create( Input::post('title'), Input::post('rating'), Input::post('review'), Input::post('review_category_id') ); + if ( ! empty( $result ) ) { + Session::flash( 'success', 'Your review has been received.' ); + Redirect::to( 'reviews/index' ); + } else { + Issues::add( 'error', 'There was an unresolved error while submitting your review.' ); + return Views::view( 'reviews.create' ); + } + } + + public function view( $id = null ) { + $review = $this->confirmOwner( $id ); + Views::view( 'reviews.view', $review ); + } + + public function edit( $id = null ) { + $review = $this->confirmOwner( $id ); + + if ( ! Input::exists( 'submit' ) ) { + return Views::view( 'reviews.edit', $review ); + } + + if ( ! Forms::check( 'reviewEditPublic' ) ) { + Issues::add( 'error', [ 'There was an error with your request.' => Check::userErrors() ] ); + return Views::view( 'reviews.edit', $review ); + } + + $result = self::$reviews->update( + $id, + Input::post('title'), + Input::post('rating'), + Input::post('review') + ); + + if ( $result ) { + Session::flash( 'success', 'Your review has been updated.' ); + return Redirect::to( 'reviews/index' ); + } + + Issues::add( 'error', [ 'There was an error with your request.' => Check::userErrors() ] ); + Views::view( 'reviews.edit', $review ); + } + + public function delete( $id = null ) { + $this->confirmOwner( $id ); + if ( self::$reviews->delete( $id ) ) { + Session::flash( 'success', 'Review deleted' ); + } else { + Session::flash( 'error', 'There was an error with your request.' ); + } + return Redirect::to( 'reviews/index' ); + } + + private function confirmOwner( $id ) { + if ( ! App::$isLoggedIn ) { + Session::flash( 'error', 'You must be logged in to review products.' ); + return Redirect::home(); + } + if ( !Check::id( $id ) ) { + Session::flash( 'error', 'Unknown Review' ); + return Redirect::to( 'reviews/index' ); + } + $review = self::$reviews->findById( $id ); + if ( empty( $review ) ) { + Session::flash( 'error', 'Unknown Review' ); + return Redirect::to( 'reviews/index' ); + } + if ( App::$activeUser->ID != $review->createdBy ) { + Session::flash( 'error', 'Unknown Review' ); + return Redirect::to( 'reviews/index' ); + } + return $review; + } +} \ No newline at end of file diff --git a/app/plugins/reviews/css/reviews.css b/app/plugins/reviews/css/reviews.css new file mode 100644 index 0000000..30d7809 --- /dev/null +++ b/app/plugins/reviews/css/reviews.css @@ -0,0 +1,9 @@ + + .star-rating .fa-star { + font-size: 24px; + color: grey; + cursor: pointer; + } + .star-rating .fa-star.checked { + color: gold; + } \ No newline at end of file diff --git a/app/plugins/reviews/forms.php b/app/plugins/reviews/forms.php new file mode 100644 index 0000000..4f7ed41 --- /dev/null +++ b/app/plugins/reviews/forms.php @@ -0,0 +1,109 @@ + + * @link https://TheTempusProject.com + * @license https://opensource.org/licenses/MIT [MIT LICENSE] + */ +namespace TheTempusProject\Plugins\Reviews; + +use TheTempusProject\Bedrock\Functions\Input; +use TheTempusProject\Bedrock\Functions\Check; +use TheTempusProject\Classes\Forms; + +class ReviewForms extends Forms { + /** + * Adds these functions to the form list. + */ + public function __construct() { + self::addHandler( 'createReview', __CLASS__, 'createReview' ); + self::addHandler( 'editReview', __CLASS__, 'editReview' ); + self::addHandler( 'categoryCreate', __CLASS__, 'categoryCreate' ); + self::addHandler( 'categoryEdit', __CLASS__, 'categoryEdit' ); + self::addHandler( 'reviewEditPublic', __CLASS__, 'reviewEditPublic' ); + } + + /** + * Validates the createReview form. + * + * @return {bool} + */ + public static function createReview() { + if ( ! Input::exists( 'review' ) ) { + Check::addUserError( 'You must provide a review.' ); + return false; + } + if ( ! Input::exists( 'title' ) ) { + Check::addUserError( 'You must provide a title.' ); + return false; + } + if ( ! Input::exists( 'rating' ) ) { + Check::addUserError( 'You must provide a rating.' ); + return false; + } + if ( ! Input::exists( 'review_category_id' ) ) { + Check::addUserError( 'You must provide a review_category_id.' ); + return false; + } + return true; + } + public static function editReview() { + if ( ! Input::exists( 'review' ) ) { + Check::addUserError( 'You must provide a review.' ); + return false; + } + if ( ! Input::exists( 'title' ) ) { + Check::addUserError( 'You must provide a title.' ); + return false; + } + if ( ! Input::exists( 'rating' ) ) { + Check::addUserError( 'You must provide a rating.' ); + return false; + } + return true; + } + public static function categoryCreate() { + if ( ! Input::exists( 'name' ) ) { + Check::addUserError( 'You must provide a name.' ); + return false; + } + if ( ! Input::exists( 'slug' ) ) { + Check::addUserError( 'You must provide a slug.' ); + return false; + } + return true; + } + public static function categoryEdit() { + if ( ! Input::exists( 'name' ) ) { + Check::addUserError( 'You must provide a name.' ); + return false; + } + if ( ! Input::exists( 'slug' ) ) { + Check::addUserError( 'You must provide a slug.' ); + return false; + } + return true; + } + public static function reviewEditPublic() { + if ( ! Input::exists( 'review' ) ) { + Check::addUserError( 'You must provide a review.' ); + return false; + } + if ( ! Input::exists( 'title' ) ) { + Check::addUserError( 'You must provide a title.' ); + return false; + } + if ( ! Input::exists( 'rating' ) ) { + Check::addUserError( 'You must provide a rating.' ); + return false; + } + return true; + } +} + +new ReviewForms; diff --git a/app/plugins/reviews/js/reviews.js b/app/plugins/reviews/js/reviews.js new file mode 100644 index 0000000..dfd6ad2 --- /dev/null +++ b/app/plugins/reviews/js/reviews.js @@ -0,0 +1,20 @@ +$(document).ready(function() { + var $star_rating = $('.star-rating .fa-star'); + + var SetRatingStar = function() { + return $star_rating.each(function() { + if (parseInt($(this).siblings('input.rating-value').val()) >= parseInt($(this).data('rating'))) { + return $(this).addClass('checked'); + } else { + return $(this).removeClass('checked'); + } + }); + }; + + $star_rating.on('click', function() { + $(this).siblings('input.rating-value').val($(this).data('rating')); + return SetRatingStar(); + }); + + SetRatingStar(); +}); \ No newline at end of file diff --git a/app/plugins/reviews/models/review.php b/app/plugins/reviews/models/review.php new file mode 100644 index 0000000..400fb0e --- /dev/null +++ b/app/plugins/reviews/models/review.php @@ -0,0 +1,164 @@ + + * @link https://TheTempusProject.com + * @license https://opensource.org/licenses/MIT [MIT LICENSE] + */ +namespace TheTempusProject\Models; + +use TheTempusProject\Bedrock\Classes\Config; +use TheTempusProject\Bedrock\Functions\Check; +use TheTempusProject\Canary\Bin\Canary as Debug; +use TheTempusProject\Classes\DatabaseModel; +use TheTempusProject\Plugins\Reviews as Plugin; +use TheTempusProject\TheTempusProject as App; +use TheTempusProject\Canary\Classes\CustomException; + +class Review extends DatabaseModel { + public $tableName = 'reviews'; + public $databaseMatrix = [ + [ 'review_category_id', 'int', '11' ], + [ 'title', 'varchar', '128' ], + [ 'rating', 'int', '1' ], + [ 'review', 'text', '' ], + [ 'createdBy', 'int', '11' ], + [ 'createdAt', 'int', '11' ], + [ 'approvedAt', 'int', '11' ], + [ 'approvedBy', 'int', '11' ], + ]; + public $plugin; + + /** + * The model constructor. + */ + public function __construct() { + parent::__construct(); + $this->plugin = new Plugin; + } + + public function create( $title, $rating, $review, $review_category_id = '0' ) { + if ( ! $this->plugin->checkEnabled() ) { + Debug::info( 'Reviews are disabled in the config.' ); + return false; + } + $fields = [ + 'title' => $title, + 'rating' => $rating, + 'review' => $review, + 'review_category_id' => $review_category_id, + 'createdAt' => time(), + 'createdBy' => App::$activeUser->ID, + ]; + if ( ! self::$db->insert( $this->tableName, $fields ) ) { + Debug::info( 'Reviews::create - failed to insert to db' ); + return false; + } + return self::$db->lastId(); + } + + public function update( $id, $title, $rating, $review, $review_category_id = null ) { + if ( ! $this->plugin->checkEnabled() ) { + Debug::info( 'Reviews are disabled in the config.' ); + return false; + } + $fields = [ + 'title' => $title, + 'rating' => $rating, + 'review' => $review + ]; + if ( ! empty( $review_category_id ) ) { + $fields['review_category_id'] = $review_category_id; + } + if ( !self::$db->update( $this->tableName, $id, $fields ) ) { + new CustomException( 'reviewUpdate' ); + Debug::error( "Review: $id not updated: $fields" ); + return false; + } + return true; + } + + public function approve( $ID ) { + if ( !Check::id( $ID ) ) { + Debug::info( 'Review Categories: illegal ID.' ); + return false; + } + $fields = [ + 'approvedAt' => time(), + 'approvedBy' => App::$activeUser->ID, + ]; + if ( !self::$db->update( $this->tableName, $ID, $fields ) ) { + new CustomException( $this->tableName ); + Debug::error( "Review Categories: $ID Approved: $fields" ); + return false; + } + return true; + } + + public function hide( $ID ) { + // if ( !Check::id( $ID ) ) { + // Debug::info( 'Review Categories: illegal ID.' ); + // return false; + // } + // $fields = [ + // 'approvedAt' => null, + // 'approvedBy' => null, + // ]; + // if ( !self::$db->update( $this->tableName, $ID, $fields ) ) { + // new CustomException( $this->tableName ); + // Debug::error( "Review Categories: $ID not Approved: $fields" ); + // return false; + // } + return true; + } + + public function byUser( $limit = null ) { + $whereClause = ['createdBy', '=', App::$activeUser->ID]; + if ( empty( $limit ) ) { + $reviews = self::$db->get( $this->tableName, $whereClause ); + } else { + $reviews = self::$db->get( $this->tableName, $whereClause, 'ID', 'DESC', [0, $limit] ); + } + if ( !$reviews->count() ) { + Debug::info( 'No Reviews found.' ); + return false; + } + return $this->filter( $reviews->results() ); + } + + public function reviewedCategoriesByUser() { + $whereClause = ['createdBy', '=', App::$activeUser->ID]; + $reviews = self::$db->action( 'SELECT ID,review_category_id', $this->tableName, $whereClause ); + + if ( !$reviews->count() ) { + Debug::info( 'No Reviews found.' ); + return false; + } + $categoryIds = []; + foreach ($reviews->results() as $key => $result) { + if ( ! in_array( $result->review_category_id, $categoryIds ) ) { + $categoryIds[] = $result->review_category_id; + } + } + return $categoryIds; + } + + public function byCategory( $id, $limit = null ) { + $whereClause = [ 'review_category_id', '=', $id ]; + if ( empty( $limit ) ) { + $reviews = self::$db->get( $this->tableName, $whereClause ); + } else { + $reviews = self::$db->get( $this->tableName, $whereClause, 'ID', 'DESC', [0, $limit] ); + } + if ( !$reviews->count() ) { + Debug::info( 'No Reviews found.' ); + return false; + } + return $this->filter( $reviews->results() ); + } +} diff --git a/app/plugins/reviews/models/review_category.php b/app/plugins/reviews/models/review_category.php new file mode 100644 index 0000000..933b722 --- /dev/null +++ b/app/plugins/reviews/models/review_category.php @@ -0,0 +1,138 @@ + + * @link https://TheTempusProject.com + * @license https://opensource.org/licenses/MIT [MIT LICENSE] + */ +namespace TheTempusProject\Models; + +use TheTempusProject\Bedrock\Classes\Config; +use TheTempusProject\Bedrock\Functions\Check; +use TheTempusProject\Canary\Bin\Canary as Debug; +use TheTempusProject\Classes\DatabaseModel; +use TheTempusProject\Plugins\Reviews as Plugin; +use TheTempusProject\Models\Review; +use TheTempusProject\TheTempusProject as App; +use TheTempusProject\Canary\Classes\CustomException; + +class ReviewCategory extends DatabaseModel { + public $tableName = 'review_categories'; + public $databaseMatrix = [ + [ 'name', 'varchar', '128' ], + [ 'slug', 'varchar', '64' ], + [ 'createdBy', 'int', '11' ], + [ 'createdAt', 'int', '11' ], + ]; + public $plugin; + + /** + * The model constructor. + */ + public function __construct() { + parent::__construct(); + $this->plugin = new Plugin; + } + + public function create( $name, $slug ) { + if ( ! $this->plugin->checkEnabled() ) { + Debug::info( 'Reviews are disabled in the config.' ); + return false; + } + if ( !Check::dataTitle( $slug ) ) { + Debug::info( 'Review Categories: illegal title.' ); + return false; + } + $fields = [ + 'name' => $name, + 'slug' => $slug, + 'createdAt' => time(), + 'createdBy' => App::$activeUser->ID, + ]; + if ( ! self::$db->insert( $this->tableName, $fields ) ) { + Debug::info( 'ReviewCategories::create - failed to insert to db' ); + return false; + } + return self::$db->lastId(); + } + + public function update( $id, $name, $slug ) { + if ( ! $this->plugin->checkEnabled() ) { + Debug::info( 'Reviews are disabled in the config.' ); + return false; + } + if ( !Check::dataTitle( $slug ) ) { + Debug::info( 'Review Categories: illegal title.' ); + return false; + } + $fields = [ + 'name' => $name, + 'slug' => $slug, + ]; + if ( !self::$db->update( $this->tableName, $id, $fields ) ) { + new CustomException( 'reviewCategoryUpdate' ); + Debug::error( "Review Categories: $id not updated: $fields" ); + return false; + } + return true; + } + + public function findBySlug( $slug, $limit = null ) { + $whereClause = [ 'slug', '=', $slug ]; + if ( empty( $limit ) ) { + $categories = self::$db->get( $this->tableName, $whereClause ); + } else { + $categories = self::$db->get( $this->tableName, $whereClause, 'ID', 'DESC', [0, $limit] ); + } + if ( !$categories->count() ) { + Debug::info( 'No categories found.' ); + return false; + } + return $this->filter( $categories->first() ); + } + + public function simple() { + $categories = self::$db->get( $this->tableName, '*' ); + if ( !$categories->count() ) { + Debug::warn( 'Could not find any categories' ); + return false; + } + + $categories = $categories->results(); + $out = []; + foreach ( $categories as &$category ) { + $out[ $category->name ] = $category->ID; + } + return $out; + } + + public function simpleUnreviewed() { + $categories = self::$db->get( $this->tableName, '*' ); + if ( !$categories->count() ) { + Debug::warn( 'Could not find any categories' ); + return false; + } + + $categories = $categories->results(); + + $review = new Review; + + $reviews = $review->reviewedCategoriesByUser(); + + $out = []; + foreach ( $categories as &$category ) { + + if ( ! empty( $reviews) && in_array( $category->ID, $reviews ) ) { + continue; + } + + $out[ $category->name ] = $category->ID; + } + return $out; + } +} diff --git a/app/plugins/reviews/plugin.php b/app/plugins/reviews/plugin.php new file mode 100644 index 0000000..77e9314 --- /dev/null +++ b/app/plugins/reviews/plugin.php @@ -0,0 +1,51 @@ + + * @link https://TheTempusProject.com + * @license https://opensource.org/licenses/MIT [MIT LICENSE] + */ +namespace TheTempusProject\Plugins; + +use TheTempusProject\Classes\Plugin; + +class Reviews extends Plugin { + public $pluginName = 'TP Reviews'; + public $pluginAuthor = 'JoeyK'; + public $pluginWebsite = 'https://TheTempusProject.com'; + public $modelVersion = '1.0'; + public $pluginVersion = '3.0'; + public $pluginDescription = 'A simple plugin which adds a site wide review system.'; + public $permissionMatrix = [ + 'review' => [ + 'pretty' => 'Can create reviews', + 'default' => false, + ], + 'createReviewCategory' => [ + 'pretty' => 'Can create review categories', + 'default' => false, + ], + ]; + public $admin_links = [ + [ + 'text' => ' Reviews', + 'url' => '{ROOT_URL}admin/reviews', + ], + ]; + public $contact_footer_links = [ + [ + 'text' => 'Reviews', + 'url' => '{ROOT_URL}reviews/index/', + 'filter' => 'loggedin', + ], + ]; +} + +// need a setting to allow reviewing a category from the dropdown +// if disabled, "I'm sorry but reviews are invite only. If you have been asked to review a product, double check the link and make sure its been entered correctly +// if you have not been invited to review a product, please feel free to leave any feedback in the feedback section. \ No newline at end of file diff --git a/app/plugins/reviews/views/admin/category.html b/app/plugins/reviews/views/admin/category.html new file mode 100644 index 0000000..841f402 --- /dev/null +++ b/app/plugins/reviews/views/admin/category.html @@ -0,0 +1,42 @@ +
    +
    +
    + {ADMIN_BREADCRUMBS} +
    + +
    +

    Review Category: {name}

    +
    + + +
    +
    + + + + + + + + + + + + + + + + +
    Name:{name}
    Slug:{slug}
    Created:{DTC}{createdAt}{/DTC}
    +
    +
    + + + +
    +
    +
    +
    \ No newline at end of file diff --git a/app/plugins/reviews/views/admin/categoryCreate.html b/app/plugins/reviews/views/admin/categoryCreate.html new file mode 100644 index 0000000..6cacb20 --- /dev/null +++ b/app/plugins/reviews/views/admin/categoryCreate.html @@ -0,0 +1,35 @@ +
    + Add Review Category +
    + {ADMIN_BREADCRUMBS} +
    +
    + +
    + +
    + +
    +
    + + +
    + +
    + + + Public links would be {ROOT_URL}reviews/YOUR_SLUG_HERE + +
    +
    + + + + + +
    + +
    +
    +
    +
    \ No newline at end of file diff --git a/app/plugins/reviews/views/admin/categoryEdit.html b/app/plugins/reviews/views/admin/categoryEdit.html new file mode 100644 index 0000000..f91060e --- /dev/null +++ b/app/plugins/reviews/views/admin/categoryEdit.html @@ -0,0 +1,35 @@ +
    + Edit Review Category +
    + {ADMIN_BREADCRUMBS} +
    +
    + +
    + +
    + +
    +
    + + +
    + +
    + + + Public links would be {ROOT_URL}reviews/YOUR_SLUG_HERE + +
    +
    + + + + + +
    + +
    +
    +
    +
    \ No newline at end of file diff --git a/app/plugins/reviews/views/admin/categoryList.html b/app/plugins/reviews/views/admin/categoryList.html new file mode 100644 index 0000000..22a5fc0 --- /dev/null +++ b/app/plugins/reviews/views/admin/categoryList.html @@ -0,0 +1,33 @@ +Review Categories + + + + + + + + + + + + + {LOOP} + + + + + + + + + {/LOOP} + {ALT} + + + + {/ALT} + +
    IDNameSlug
    {ID}{name}{slug}
    + No results to show. +
    +Create \ No newline at end of file diff --git a/app/plugins/reviews/views/admin/dashboard.html b/app/plugins/reviews/views/admin/dashboard.html new file mode 100644 index 0000000..52682fa --- /dev/null +++ b/app/plugins/reviews/views/admin/dashboard.html @@ -0,0 +1,9 @@ + +
    +
    + {reviews} +
    +
    + {reviewCategories} +
    +
    \ No newline at end of file diff --git a/app/plugins/reviews/views/admin/review.html b/app/plugins/reviews/views/admin/review.html new file mode 100644 index 0000000..4e1bb9c --- /dev/null +++ b/app/plugins/reviews/views/admin/review.html @@ -0,0 +1,49 @@ +
    +
    +
    + {ADMIN_BREADCRUMBS} +
    + +
    +

    Review: {title}

    +
    + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + +
    Title:{title}
    Rating:{rating}
    Created:{DTC}{createdAt}{/DTC}
    Review:
    {review}
    +
    +
    + + + +
    +
    +
    +
    \ No newline at end of file diff --git a/app/plugins/reviews/views/admin/reviewList.html b/app/plugins/reviews/views/admin/reviewList.html new file mode 100644 index 0000000..6cd391c --- /dev/null +++ b/app/plugins/reviews/views/admin/reviewList.html @@ -0,0 +1,32 @@ +Reviews + + + + + + + + + + + + + {LOOP} + + + + + + + + + {/LOOP} + {ALT} + + + + {/ALT} + +
    IDTitleRating
    {ID}{title}{rating}
    + No results to show. +
    \ No newline at end of file diff --git a/app/plugins/reviews/views/admin/view.html b/app/plugins/reviews/views/admin/view.html new file mode 100644 index 0000000..4e1bb9c --- /dev/null +++ b/app/plugins/reviews/views/admin/view.html @@ -0,0 +1,49 @@ +
    +
    +
    + {ADMIN_BREADCRUMBS} +
    + +
    +

    Review: {title}

    +
    + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + +
    Title:{title}
    Rating:{rating}
    Created:{DTC}{createdAt}{/DTC}
    Review:
    {review}
    +
    +
    + + + +
    +
    +
    +
    \ No newline at end of file diff --git a/app/plugins/reviews/views/create.html b/app/plugins/reviews/views/create.html new file mode 100644 index 0000000..51bd287 --- /dev/null +++ b/app/plugins/reviews/views/create.html @@ -0,0 +1,52 @@ +
    +

    Share a Review

    +
    +

    We thank you for for taking the time to review our products. We do not edit or modify reviews in any way. You, as the customer have the ability to modify your own reviews.

    +

    We read each and every review. You and the admin team both have the ability to comment on reviews privately. Neither your reviews or comments will be publicly shared without your permission.

    +
    + +
    + + {reviewCategorySelect} +
    + + +
    + +
    + + + + + + +
    +
    + + +
    + + + + No need to overthink it, something as simple as "My Review" is fine. + +
    + + +
    + + + + (max: 2000 characters) + +
    + + + + + +
    + +
    +
    +
    \ No newline at end of file diff --git a/app/plugins/reviews/views/edit.html b/app/plugins/reviews/views/edit.html new file mode 100644 index 0000000..27b5830 --- /dev/null +++ b/app/plugins/reviews/views/edit.html @@ -0,0 +1,44 @@ +
    +

    Edit Your Review

    +
    +
    + +
    + + + + No need to overthink it, something as simple as "My Review" is fine. + +
    + + +
    + +
    + + + + + + +
    +
    + + +
    + + + + (max: 2000 characters) + +
    + + + + + +
    + +
    +
    +
    \ No newline at end of file diff --git a/app/plugins/reviews/views/list.html b/app/plugins/reviews/views/list.html new file mode 100644 index 0000000..69c1653 --- /dev/null +++ b/app/plugins/reviews/views/list.html @@ -0,0 +1,38 @@ +
    + Reviews +
    +

    + Understanding the customer is a huge part of making products better. Whether its feedback that we are doing great, or we need work; the review allows users to share that with us. +

    +

    + On this page you can find your reviews to see any responses or make edits. +

    + + + + + + + + + + + {LOOP} + + + + + + + {/LOOP} + {ALT} + + + + {/ALT} + +
    TitleRating
    {title}{rating}
    + No results to show. +
    + Add Review +
    \ No newline at end of file diff --git a/app/plugins/reviews/views/view.html b/app/plugins/reviews/views/view.html new file mode 100644 index 0000000..5a5d754 --- /dev/null +++ b/app/plugins/reviews/views/view.html @@ -0,0 +1,47 @@ +
    +
    +
    +
    + +
    +

    Review: {title}

    +
    + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + +
    Title:{title}
    Rating:{rating}
    Created:{DTC}{createdAt}{/DTC}
    Review:
    {review}
    +
    +
    + + + +
    +
    +
    +
    \ No newline at end of file diff --git a/app/plugins/subscribe/views/footer/right.html b/app/plugins/subscribe/views/footer/right.html index fa833b8..e137ceb 100644 --- a/app/plugins/subscribe/views/footer/right.html +++ b/app/plugins/subscribe/views/footer/right.html @@ -1,11 +1,11 @@
    -
    Subscribe
    +
    Subscribe
    - +
    \ No newline at end of file diff --git a/app/plugins/suggestions/controllers/admin/suggestions.php b/app/plugins/suggestions/controllers/admin/suggestions.php new file mode 100644 index 0000000..ed238ba --- /dev/null +++ b/app/plugins/suggestions/controllers/admin/suggestions.php @@ -0,0 +1,119 @@ + + * @link https://TheTempusProject.com + * @license https://opensource.org/licenses/MIT [MIT LICENSE] + */ +namespace TheTempusProject\Controllers\Admin; + +use TheTempusProject\Houdini\Classes\Issues; +use TheTempusProject\Houdini\Classes\Views; +use TheTempusProject\Houdini\Classes\Components; +use TheTempusProject\Bedrock\Functions\Input; +use TheTempusProject\Bedrock\Functions\Check; +use TheTempusProject\Classes\Forms; +use TheTempusProject\Classes\AdminController; +use TheTempusProject\Models\Suggestions as SuggestionsModel; +use TheTempusProject\Models\Comments; +use TheTempusProject\Houdini\Classes\Forms as TemplateForm; + +class Suggestions extends AdminController { + protected static $suggestions; + protected static $comments; + + public function __construct() { + parent::__construct(); + self::$title = 'Admin - Suggestions'; + self::$suggestions = new SuggestionsModel; + } + + public function index( $data = null ) { + Views::view( 'suggestions.admin.list', self::$suggestions->list() ); + } + + public function view( $id = null ) { + $data = self::$suggestions->findById( $id ); + if ( $data == false ) { + Issues::add( 'error', 'Suggestion not found.' ); + return $this->index(); + } + if ( empty( self::$comments ) ) { + self::$comments = new Comments; + } + Components::set( 'COMMENT_TYPE', 'suggestions' ); + Components::set( 'count', self::$comments->count( 'suggestion', $id ) ); + Components::set( 'COMMENTS', Views::simpleView( 'comments.list', self::$comments->display( 10, 'suggestion', $id ) ) ); + Views::view( 'suggestions.admin.view', $data ); + } + + public function approve( $id = null ) { + $data = self::$suggestions->findById( $id ); + if ( $data == false ) { + Issues::add( 'error', 'Suggestion not found.' ); + return $this->index(); + } + if ( !self::$suggestions->approve( $id ) ) { + Issues::add( 'error', 'Suggestion not approved.' ); + return $this->index(); + } + Issues::add( 'success', 'Suggestion Approved' ); + return $this->index(); + } + + public function reject( $id = null ) { + $data = self::$suggestions->findById( $id ); + if ( $data == false ) { + Issues::add( 'error', 'Suggestion not found.' ); + return $this->index(); + } + if ( !self::$suggestions->reject( $id ) ) { + Issues::add( 'error', 'Suggestion not rejected.' ); + return $this->index(); + } + Issues::add( 'success', 'Suggestion Rejected' ); + return $this->index(); + } + + public function edit( $id = null ) { + $data = self::$suggestions->findById( $id ); + if ( false == $data ) { + return $this->index(); + } + TemplateForm::selectRadio( 'approved', $data->approved ); + if ( !Input::exists( 'submit' ) ) { + return Views::view( 'suggestions.admin.edit', $data ); + } + if ( !Forms::check( 'editSuggestion' ) ) { + Issues::add( 'error', [ 'There was an error with your request.' => Check::userErrors() ] ); + return Views::view( 'suggestions.admin.edit', $data ); + } + if ( self::$suggestions->update( $id, Input::post( 'title' ), Input::post( 'suggestion' ), Input::post( 'approved' ) ) ) { + Issues::add( 'success', 'Suggestion updated' ); + } else { + return Views::view( 'suggestions.edit', $data ); + } + } + + public function delete( $data = null ) { + if ( Input::exists( 'submit' ) ) { + $data = Input::post( 'S_' ); + } + if ( !self::$suggestions->delete( $data ) ) { + Issues::add( 'error', 'There was an error with your request.' ); + } else { + Issues::add( 'success', 'Suggestions has been deleted' ); + } + $this->index(); + } + + public function clear( $data = null ) { + self::$suggestions->empty(); + $this->index(); + } +} diff --git a/app/plugins/suggestions/controllers/suggestions.php b/app/plugins/suggestions/controllers/suggestions.php new file mode 100644 index 0000000..8f2efcf --- /dev/null +++ b/app/plugins/suggestions/controllers/suggestions.php @@ -0,0 +1,144 @@ + + * @link https://TheTempusProject.com + * @license https://opensource.org/licenses/MIT [MIT LICENSE] + */ +namespace TheTempusProject\Controllers; + +use TheTempusProject\Bedrock\Functions\Check; +use TheTempusProject\Bedrock\Functions\Input; +use TheTempusProject\Hermes\Functions\Redirect; +use TheTempusProject\Bedrock\Functions\Session; +use TheTempusProject\Houdini\Classes\Issues; +use TheTempusProject\Houdini\Classes\Views; +use TheTempusProject\Classes\Controller; +use TheTempusProject\Classes\Forms; +use TheTempusProject\Models\Suggestions as SuggestionModel; +use TheTempusProject\TheTempusProject as App; +use TheTempusProject\Models\Comments as CommentsModel; +use TheTempusProject\Plugins\Comments; +use TheTempusProject\Houdini\Classes\Components; + +class Suggestions extends Controller { + protected static $suggestions; + protected static $comments; + + public function __construct() { + parent::__construct(); + if ( !App::$isLoggedIn ) { + Session::flash( 'notice', 'You must be logged in to view suggestions.' ); + return Redirect::home(); + } + self::$suggestions = new SuggestionModel; + self::$title = 'Suggestions - {SITENAME}'; + self::$pageDescription = 'On this page you can view, submit, or comment-on suggestions for the site.'; + } + + public function index() { + Views::view( 'suggestions.list', self::$suggestions->recent() ); + } + + public function view( $id = null ) { + $data = self::$suggestions->findById( $id ); + if ( $data == false ) { + Issues::add( 'error', 'Suggestion not found.' ); + return $this->index(); + } + + if ( Input::exists( 'contentId' ) ) { + $this->comments( 'post', Input::post( 'contentId' ) ); + } + + Components::set( 'CONTENT_ID', $id ); + Components::set( 'COMMENT_TYPE', 'suggestions' ); + $plugin = new Comments; + if ( ! $plugin->checkEnabled() ) { + Components::set( 'NEWCOMMENT', '' ); + Components::set( 'count', '' ); + Components::set( 'COMMENTS', '' ); + } else { + $comments = new CommentsModel; + Components::set( 'count', $comments->count( 'suggestion', $id ) ); + Components::set( 'COMMENTS', Views::simpleView( 'comments.list', $comments->display( 10, self::$suggestions->tableName, $id ) ) ); + Components::set( 'NEWCOMMENT', Views::simpleView( 'comments.create' ) ); + } + Views::view( 'suggestions.view', $data ); + } + + public function create() { + if ( !Input::exists() ) { + return Views::view( 'suggestions.create' ); + } + if ( !Forms::check( 'newSuggestion' ) ) { + Issues::add( 'error', [ 'There was an error with your suggestion.' => Check::userErrors() ] ); + return Views::view( 'suggestions.create' ); + } + if ( !self::$suggestions->create( Input::post( 'title' ), Input::post( 'suggestion' ) ) ) { + Issues::add( 'error', [ 'There was an error with your suggestion.' => Check::userErrors() ] ); + return Views::view( 'suggestions.create' ); + } + Session::flash( 'success', 'Your Suggestion has been received. We must verify all suggestions before they are published, but as long as it doesn\'t violate our code of conduct, it should be available to view and comment on publicly within 24 hours.' ); + Redirect::to( 'suggestions/index' ); + } + + public function edit( $data = null ) { + if ( !Input::exists( 'submit' ) ) { + return Views::view( 'suggestions.edit', self::$suggestions->findById( $data ) ); + } + if ( !Forms::check( 'editSuggestion' ) ) { + Issues::add( 'error', [ 'There was an error with your request.' => Check::userErrors() ] ); + return Views::view( 'suggestions.edit', self::$suggestions->findById( $data ) ); + } + if ( self::$suggestions->update( $data, Input::post( 'title' ), Input::post( 'suggestion' ) ) ) { + Issues::add( 'success', 'Suggestion updated' ); + } else { + return Views::view( 'suggestions.edit', self::$suggestions->findById( $data ) ); + } + $this->index(); + } + + public function comments( $sub = null, $data = null ) { + if ( empty( $sub ) || empty( $data ) ) { + Issues::add( 'error', 'There was an issue with your request. Please check the url and try again.' ); + return $this->index(); + } + + $plugin = new Comments; + if ( ! $plugin->checkEnabled() ) { + Issues::add( 'error', 'Comments are disabled.' ); + return $this->index(); + } + + switch ( $sub ) { + case 'post': + $content = self::$suggestions->findById( $data ); + if ( empty( $content ) ) { + Issues::add( 'error', 'Unknown Content.' ); + return $this->index(); + } + Redirect::to( 'suggestions' ); + return $plugin->formPost( self::$suggestions->tableName, $content, 'suggestions/view/' ); + case 'edit': + $content = self::$comments->findById( $data ); + if ( empty( $content ) ) { + Issues::add( 'error', 'Unknown Comment.' ); + return $this->index(); + } + return $plugin->formEdit( self::$suggestions->tableName, $content, 'suggestions/view/' ); + case 'delete': + $content = self::$comments->findById( $data ); + if ( empty( $content ) ) { + Issues::add( 'error', 'Unknown Comment.' ); + return $this->index(); + } + return $plugin->formDelete( self::$suggestions->tableName, $content, 'suggestions/view/' ); + } + } +} \ No newline at end of file diff --git a/app/plugins/suggestions/forms.php b/app/plugins/suggestions/forms.php new file mode 100644 index 0000000..2b206b0 --- /dev/null +++ b/app/plugins/suggestions/forms.php @@ -0,0 +1,68 @@ + + * @link https://TheTempusProject.com + * @license https://opensource.org/licenses/MIT [MIT LICENSE] + */ +namespace TheTempusProject\Plugins\Suggestions; + +use TheTempusProject\Bedrock\Functions\Input; +use TheTempusProject\Classes\Forms; + +class SuggestionForms extends Forms { + /** + * Adds these functions to the form list. + */ + public function __construct() { + self::addHandler( 'newSuggestion', __CLASS__, 'newSuggestion' ); + self::addHandler( 'editSuggestion', __CLASS__, 'editSuggestion' ); + } + + /** + * Validates the suggestion create form. + * + * @return {bool} + */ + public static function newSuggestion() { + if ( !Input::exists( 'title' ) ) { + self::addUserError( 'You must specify title' ); + return false; + } + if ( !Input::exists( 'suggestion' ) ) { + self::addUserError( 'You must write a Suggestion' ); + return false; + } + if ( !self::token() ) { + return false; + } + return true; + } + + /** + * Validates the suggestion create form. + * + * @return {bool} + */ + public static function editSuggestion() { + if ( !Input::exists( 'title' ) ) { + self::addUserError( 'You must specify title' ); + return false; + } + if ( !Input::exists( 'suggestion' ) ) { + self::addUserError( 'You must write a Suggestion' ); + return false; + } + if ( !self::token() ) { + return false; + } + return true; + } +} + +new SuggestionForms; diff --git a/app/plugins/suggestions/models/suggestions.php b/app/plugins/suggestions/models/suggestions.php new file mode 100644 index 0000000..6ed6de8 --- /dev/null +++ b/app/plugins/suggestions/models/suggestions.php @@ -0,0 +1,183 @@ + + * @link https://TheTempusProject.com + * @license https://opensource.org/licenses/MIT [MIT LICENSE] + */ +namespace TheTempusProject\Models; + +use TheTempusProject\Bedrock\Functions\Check; +use TheTempusProject\Bedrock\Classes\Config; +use TheTempusProject\Canary\Bin\Canary as Debug; +use TheTempusProject\Canary\Classes\CustomException; +use TheTempusProject\Classes\DatabaseModel; +use TheTempusProject\TheTempusProject as App; + +class Suggestions extends DatabaseModel { + public $tableName = 'suggestions'; + public $databaseMatrix = [ + [ 'title', 'varchar', '86' ], + [ 'suggestion', 'text', '' ], + [ 'suggestedOn', 'int', '10' ], + [ 'approved', 'varchar', '5' ], + [ 'approvedOn', 'int', '10' ], + [ 'approvedBy', 'int', '11' ], + [ 'author', 'int', '11' ], + ]; + + /** + * The model constructor. + */ + public function __construct() { + parent::__construct(); + } + + // @TODO: Add a config for some of these things + // public function enabled() { + // if ( true === parent::enabled() ) { + // return Config::getValue( $this->configName . '/enabled' ) === true; + // } + // return false; + // } + public function filter( $data, $params = [] ) { + foreach ( $data as $instance ) { + if ( !is_object( $instance ) ) { + $instance = $data; + $end = true; + } + if ( !empty($instance->approvedBy)) { + $instance->approvedByName = self::$user->getUsername( $instance->approvedBy ); + } else { + $instance->approvedByName = ''; + } + $instance->submittedBy = self::$user->getUsername( $instance->author ); + $out[] = $instance; + if ( !empty( $end ) ) { + $out = $out[0]; + break; + } + } + return $out; + } + + /** + * Logs a Suggestion form. + * + * @param int $ID the user ID submitting the form + * @param string $url the url + * @param string $o_url the original url + * @param int $repeat is repeatable? + * @param string $description_ description of the event. + * + * @return null + */ + public function create( $title, $suggestion, $approved = 'false' ) { + if ( !$this->enabled() ) { + Debug::info( 'Suggestions are disabled in the config.' ); + return false; + } + $fields = [ + 'author' => App::$activeUser->ID, + 'suggestedOn' => time(), + 'suggestion' => $suggestion, + 'approved' => $approved, + 'title' => $title, + ]; + if ( !self::$db->insert( $this->tableName, $fields ) ) { + new CustomException( $this->tableName ); + return false; + } + return self::$db->lastId(); + } + + public function update( $ID, $title, $suggestion, $approved = 'false' ) { + if ( empty( self::$log ) ) { + self::$log = new Log; + } + if ( !Check::id( $ID ) ) { + Debug::info( 'Suggestion: illegal ID.' ); + return false; + } + $fields = [ + 'suggestion' => $suggestion, + 'approved' => $approved, + 'title' => $title, + ]; + if ( !self::$db->update( $this->tableName, $ID, $fields ) ) { + new CustomException( $this->tableName ); + Debug::error( "Suggestion: $ID not updated: $fields" ); + + return false; + } + self::$log->admin( "Updated Suggestion: $ID" ); + return true; + } + + public function approve( $ID ) { + if ( empty( self::$log ) ) { + self::$log = new Log; + } + if ( !Check::id( $ID ) ) { + Debug::info( 'Suggestion: illegal ID.' ); + return false; + } + $fields = [ + 'approved' => 'true', + 'approvedOn' => time(), + 'approvedBy' => App::$activeUser->ID, + ]; + if ( !self::$db->update( $this->tableName, $ID, $fields ) ) { + new CustomException( $this->tableName ); + Debug::error( "Suggestion: $ID not Rejected: $fields" ); + return false; + } + self::$log->admin( "Suggestion Rejected: $ID" ); + return true; + } + + public function reject( $ID ) { + if ( empty( self::$log ) ) { + self::$log = new Log; + } + if ( !Check::id( $ID ) ) { + Debug::info( 'Suggestion: illegal ID.' ); + return false; + } + $fields = [ + 'approved' => 'false', + 'approvedOn' => null, + 'approvedBy' => null, + ]; + if ( !self::$db->update( $this->tableName, $ID, $fields ) ) { + new CustomException( $this->tableName ); + Debug::error( "Suggestion: $ID not Approved: $fields" ); + return false; + } + self::$log->admin( "Suggestion Approved: $ID" ); + return true; + } + + public function recent( $approvedOnly = true, $limit = 0 ) { + if ( true === $approvedOnly ) { + $where = ['approved', '=', 'true']; + } else { + $where = ['ID', '>', '0']; + } + if ( empty( $limit ) ) { + $data = self::$db->getPaginated( $this->tableName, $where, 'suggestedOn', 'DESC' ); + } else { + $data = self::$db->get( $this->tableName, $where, 'suggestedOn', 'DESC', [0, $limit] ); + } + if ( !$data->count() ) { + Debug::info( 'No Suggestions found.' ); + return false; + } + return $this->filter( $data->results() ); + } +} diff --git a/app/plugins/suggestions/plugin.php b/app/plugins/suggestions/plugin.php new file mode 100644 index 0000000..817a5a6 --- /dev/null +++ b/app/plugins/suggestions/plugin.php @@ -0,0 +1,47 @@ + + * @link https://TheTempusProject.com + * @license https://opensource.org/licenses/MIT [MIT LICENSE] + */ +namespace TheTempusProject\Plugins; + +use ReflectionClass; +use TheTempusProject\Classes\Installer; +use TheTempusProject\Houdini\Classes\Navigation; +use TheTempusProject\Classes\Plugin; +use TheTempusProject\TheTempusProject as App; + +class Suggestions extends Plugin { + public $pluginName = 'TP Suggest'; + public $pluginAuthor = 'JoeyK'; + public $pluginWebsite = 'https://TheTempusProject.com'; + public $modelVersion = '1.0'; + public $pluginVersion = '3.0'; + public $pluginDescription = 'A simple plugin which adds a site wide suggestion system.'; + public $permissionMatrix = [ + 'suggest' => [ + 'pretty' => 'Can create suggestions', + 'default' => false, + ], + ]; + public $admin_links = [ + [ + 'text' => ' Suggestions', + 'url' => '{ROOT_URL}admin/suggestions', + ], + ]; + public $contact_footer_links = [ + [ + 'text' => 'Suggestions', + 'url' => '{ROOT_URL}suggestions/index', + 'filter' => 'loggedin', + ], + ]; +} diff --git a/app/plugins/suggestions/views/admin/edit.html b/app/plugins/suggestions/views/admin/edit.html new file mode 100644 index 0000000..95e0976 --- /dev/null +++ b/app/plugins/suggestions/views/admin/edit.html @@ -0,0 +1,38 @@ +
    +
    + Edit Suggestion +
    +
    +
    +
    + +
    + +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    + +
    +
    + + + + + +
    + +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/app/plugins/suggestions/views/admin/list.html b/app/plugins/suggestions/views/admin/list.html new file mode 100644 index 0000000..4167cbf --- /dev/null +++ b/app/plugins/suggestions/views/admin/list.html @@ -0,0 +1,59 @@ +
    + Suggestions +
    + {ADMIN_BREADCRUMBS} +
    + + + + + + + + + + + + + + + + + {LOOP} + + + + + + + + + + + + + {/LOOP} + {ALT} + + + + {/ALT} + +
    IDSuggestionSuggested OnSuggested ByApprovedApproved OnApproved By + +
    {ID}{suggestion}{DTC}{suggestedOn}{/DTC}{submittedBy}{approved}{DTC}{approvedOn}{/DTC}{approvedByName} + + + + + + + +
    + No results to show. +
    + +
    +
    + clear all +
    \ No newline at end of file diff --git a/app/plugins/suggestions/views/admin/view.html b/app/plugins/suggestions/views/admin/view.html new file mode 100644 index 0000000..788d9ea --- /dev/null +++ b/app/plugins/suggestions/views/admin/view.html @@ -0,0 +1,14 @@ +
    + Suggestion: {title} +
    + {ADMIN_BREADCRUMBS} + + {suggestion} + {ADMIN} +
    + + +
    + {/ADMIN} + {COMMENTS} +
    \ No newline at end of file diff --git a/app/plugins/suggestions/views/create.html b/app/plugins/suggestions/views/create.html new file mode 100644 index 0000000..08e419a --- /dev/null +++ b/app/plugins/suggestions/views/create.html @@ -0,0 +1,32 @@ +
    +

    Make a suggestion

    +

    + I can't be expected to come up with all the great ideas around here. Feel free to make a suggestion here. Some suggestions will be approved for the community to comment on! +

    +
    + +
    + +
    + +
    +
    + + +
    + +
    + + Max: 2000 characters +
    +
    + + + + + +
    + +
    +
    +
    diff --git a/app/plugins/suggestions/views/edit.html b/app/plugins/suggestions/views/edit.html new file mode 100644 index 0000000..63d2d70 --- /dev/null +++ b/app/plugins/suggestions/views/edit.html @@ -0,0 +1,15 @@ +
    +
    + +
    + +
    +
    +
    +
    + +
    +
    + + +
    \ No newline at end of file diff --git a/app/plugins/suggestions/views/list.html b/app/plugins/suggestions/views/list.html new file mode 100644 index 0000000..20a402a --- /dev/null +++ b/app/plugins/suggestions/views/list.html @@ -0,0 +1,26 @@ +
    + Suggestions +
    + {LOOP} +
    +

    {title}

    +
    + {suggestion} + +
    +
    +
    +
    +
    + {/LOOP} + {ALT} +
    + +
    + {/ALT} + +
    \ No newline at end of file diff --git a/app/plugins/suggestions/views/view.html b/app/plugins/suggestions/views/view.html new file mode 100644 index 0000000..985d782 --- /dev/null +++ b/app/plugins/suggestions/views/view.html @@ -0,0 +1,20 @@ +
    +
    +
    +
    +

    {title}

    +
    + + {suggestion} + {ADMIN} +
    + + +
    + {/ADMIN} +
    + {COMMENTS} + {NEWCOMMENT} +
    +
    +
    diff --git a/app/plugins/wip/controllers/admin/wip.php b/app/plugins/wip/controllers/admin/wip.php new file mode 100644 index 0000000..6c4ddb4 --- /dev/null +++ b/app/plugins/wip/controllers/admin/wip.php @@ -0,0 +1,102 @@ + + * @link https://TheTempusProject.com + * @license https://opensource.org/licenses/MIT [MIT LICENSE] + */ +namespace TheTempusProject\Controllers\Admin; + +use TheTempusProject\Bedrock\Functions\Check; +use TheTempusProject\Bedrock\Functions\Input; +use TheTempusProject\Houdini\Classes\Issues; +use TheTempusProject\Houdini\Classes\Views; +use TheTempusProject\Houdini\Classes\Navigation; +use TheTempusProject\Houdini\Classes\Components; +use TheTempusProject\Classes\AdminController; +use TheTempusProject\Classes\Forms; +use TheTempusProject\Models\Projects; + +class Wip extends AdminController { + public static $projects; + + public function __construct() { + parent::__construct(); + self::$projects = new Projects; + self::$title = 'Admin - WIP'; + $view = Navigation::activePageSelect( 'nav.admin', '/admin/wip' ); + Components::set( 'ADMINNAV', $view ); + } + + public function index( $data = null ) { + Views::view( 'wip.admin.list', self::$projects->listPaginated() ); + } + + public function create( $data = null ) { + if ( !Input::exists( 'submit' ) ) { + return Views::view( 'wip.admin.create' ); + } + if ( !Forms::check( 'newProject' ) ) { + Issues::add( 'error', [ 'There was an error with your request.' => Check::userErrors() ] ); + return $this->index(); + } + $result = self::$projects->create( Input::post( 'title' ), Input::post( 'description' ), Input::post( 'progress' ), Input::post( 'startDate' ) ); + if ( $result ) { + Issues::add( 'success', 'Your projects has been created.' ); + return $this->index(); + } else { + Issues::add( 'error', [ 'There was an unknown error submitting your data.' => Check::userErrors() ] ); + return $this->index(); + } + } + + public function edit( $data = null ) { + if ( !Input::exists( 'submit' ) ) { + return Views::view( 'wip.admin.edit', self::$projects->findById( $data ) ); + } + if ( !Forms::check( 'editProject' ) ) { + Issues::add( 'error', [ 'There was an error with your form.' => Check::userErrors() ] ); + return $this->index(); + } + $fields = [ + 'title' => Input::post( 'title' ), + 'description' => Input::post( 'description' ), + 'progress' => Input::post( 'progress' ), + 'startDate' => Input::post( 'startDate' ), + ]; + if ( self::$projects->update( $data, $fields ) ) { + Issues::add( 'success', 'Projects Updated.' ); + return $this->index(); + } + Issues::add( 'error', 'There was an error with your request.' ); + $this->index(); + } + + public function view( $data = null ) { + $projectData = self::$projects->findById( $data ); + if ( $projectData !== false ) { + return Views::view( 'wip.admin.view', $projectData ); + } + Issues::add( 'error', 'Projects not found.' ); + $this->index(); + } + + public function delete( $data = null ) { + if ( $data == null ) { + if ( Input::exists( 'P_' ) ) { + $data = Input::post( 'P_' ); + } + } + if ( !self::$projects->delete( (array) $data ) ) { + Issues::add( 'error', 'There was an error with your request.' ); + } else { + Issues::add( 'success', 'Projects has been deleted' ); + } + $this->index(); + } +} diff --git a/app/plugins/wip/controllers/wip.php b/app/plugins/wip/controllers/wip.php new file mode 100644 index 0000000..374602a --- /dev/null +++ b/app/plugins/wip/controllers/wip.php @@ -0,0 +1,36 @@ + + * @link https://TheTempusProject.com + * @license https://opensource.org/licenses/MIT [MIT LICENSE] + */ +namespace TheTempusProject\Controllers; + +use TheTempusProject\Houdini\Classes\Issues; +use TheTempusProject\Houdini\Classes\Views; +use TheTempusProject\Classes\Controller; +use TheTempusProject\Models\Projects; + +class Wip extends Controller { + protected static $projects; + + public function index() { + self::$projects = new Projects; + self::$title = '{SITENAME} - Works in Progress'; + self::$pageDescription = 'Its not the longest wip in the world, but I\'m certainly proud of it.'; + + $projects = self::$projects->listPaginated(); + if ( false == $projects ) { + Issues::add( 'error', 'Well, this is embarrassing, surely he wouldn\'t just have no wip..... right.... Dave? ... erm Joey?' ); + return; + } else { + Views::view( 'wip.wip', self::$projects->listPaginated() ); + } + } +} diff --git a/app/plugins/wip/forms.php b/app/plugins/wip/forms.php new file mode 100644 index 0000000..5dcb3c7 --- /dev/null +++ b/app/plugins/wip/forms.php @@ -0,0 +1,79 @@ + + * @link https://TheTempusProject.com + * @license https://opensource.org/licenses/MIT [MIT LICENSE] + */ +namespace TheTempusProject\Plugins\Wip; + +use TheTempusProject\Bedrock\Functions\Input; +use TheTempusProject\Bedrock\Functions\Check; +use TheTempusProject\Classes\Forms; + +class WipForms extends Forms { + /** + * Adds these functions to the form list. + */ + public function __construct() { + self::addHandler( 'newProject', __CLASS__, 'newProject' ); + self::addHandler( 'editProject', __CLASS__, 'editProject' ); + } + + /** + * Validates the new project post form. + * + * @return {bool} + */ + public static function newProject() { + if ( !Input::exists( 'title' ) ) { + self::addUserError( 'You must specify title' ); + return false; + } + if ( !Input::exists( 'description' ) ) { + self::addUserError( 'You must specify description' ); + return false; + } + if ( !Input::exists( 'progress' ) ) { + self::addUserError( 'You must specify progress' ); + return false; + } + if ( !Input::exists( 'startDate' ) ) { + self::addUserError( 'You must specify startDate' ); + return false; + } + return true; + } + + /** + * Validates the edit project post form. + * + * @return {bool} + */ + public static function editProject() { + if ( !Input::exists( 'title' ) ) { + self::addUserError( 'You must specify title' ); + return false; + } + if ( !Input::exists( 'description' ) ) { + self::addUserError( 'You must specify description' ); + return false; + } + if ( !Input::exists( 'progress' ) ) { + self::addUserError( 'You must specify progress' ); + return false; + } + if ( !Input::exists( 'startDate' ) ) { + self::addUserError( 'You must specify startDate' ); + return false; + } + return true; + } +} + +new WipForms; diff --git a/app/plugins/wip/models/projects.php b/app/plugins/wip/models/projects.php new file mode 100644 index 0000000..3968843 --- /dev/null +++ b/app/plugins/wip/models/projects.php @@ -0,0 +1,85 @@ + + * @link https://TheTempusProject.com + * @license https://opensource.org/licenses/MIT [MIT LICENSE] + */ +namespace TheTempusProject\Models; + +use TheTempusProject\Bedrock\Classes\Config; +use TheTempusProject\Bedrock\Functions\Check; +use TheTempusProject\Canary\Bin\Canary as Debug; +use TheTempusProject\Classes\DatabaseModel; +use TheTempusProject\Plugins\Wip as Plugin; + +class Projects extends DatabaseModel { + public $tableName = 'projects'; + public $databaseMatrix = [ + [ 'title', 'varchar', '128' ], + [ 'description', 'text', '' ], + [ 'progress', 'int', '3' ], + [ 'startDate', 'varchar', '32' ], + ]; + public $plugin; + + /** + * The model constructor. + */ + public function __construct() { + parent::__construct(); + $this->plugin = new Plugin; + } + + public function create( $title, $description, $progress, $startDate ) { + if ( !$this->plugin->checkEnabled() ) { + Debug::info( 'Wip is disabled in the config.' ); + return false; + } + $fields = [ + 'title' => $title, + 'description' => $description, + 'progress' => $progress, + 'startDate' => $startDate, + ]; + if ( !self::$db->insert( $this->tableName, $fields ) ) { + Debug::info( 'Projects::create - failed to insert to db' ); + return false; + } + return self::$db->lastId(); + } + + public function update( $id, $fields ) { + if ( !Check::id( $id ) ) { + Debug::info( 'Project:update: illegal ID.' ); + return false; + } + if ( !self::$db->update( $this->tableName, $id, $fields ) ) { + // new CustomException( 'projectUpdate' ); + Debug::error( "Project:update: $id not updated: $fields" ); + return false; + } + return true; + } + + public function filter( $postArray, $params = [] ) { + foreach ( $postArray as $instance ) { + if ( !is_object( $instance ) ) { + $instance = $postArray; + $end = true; + } + $instance->prettyStart = $instance->startDate; + $out[] = $instance; + if ( !empty( $end ) ) { + $out = $out[0]; + break; + } + } + return $out; + } +} diff --git a/app/plugins/wip/plugin.php b/app/plugins/wip/plugin.php new file mode 100644 index 0000000..845cdfe --- /dev/null +++ b/app/plugins/wip/plugin.php @@ -0,0 +1,44 @@ + + * @link https://TheTempusProject.com + * @license https://opensource.org/licenses/MIT [MIT LICENSE] + */ +namespace TheTempusProject\Plugins; + +use TheTempusProject\Classes\Plugin; + +class Wip extends Plugin { + public $pluginName = 'TP Wip'; + public $pluginAuthor = 'JoeyK'; + public $pluginWebsite = 'https://TheTempusProject.com'; + public $modelVersion = '1.0'; + public $pluginVersion = '3.0'; + public $pluginDescription = 'A simple plugin which adds management for a wip.'; + public $configName = 'wip'; + public $configMatrix = [ + 'enabled' => [ + 'type' => 'radio', + 'pretty' => 'Enable the wip Feature.', + 'default' => true, + ], + ]; + public $main_links = [ + [ + 'text' => 'WIP', + 'url' => '{ROOT_URL}wip/index', + ], + ]; + public $admin_links = [ + [ + 'text' => ' Wip', + 'url' => '{ROOT_URL}admin/wip', + ], + ]; +} diff --git a/app/plugins/wip/views/admin/create.html b/app/plugins/wip/views/admin/create.html new file mode 100644 index 0000000..6812650 --- /dev/null +++ b/app/plugins/wip/views/admin/create.html @@ -0,0 +1,48 @@ +
    + Add WIP +
    + {ADMIN_BREADCRUMBS} +
    +
    + +
    + +
    + +
    +
    + + +
    + +
    + +
    +
    + + +
    + +
    + +
    +
    + + +
    + +
    + +
    +
    + + + + + +
    + +
    +
    +
    +
    \ No newline at end of file diff --git a/app/plugins/wip/views/admin/edit.html b/app/plugins/wip/views/admin/edit.html new file mode 100644 index 0000000..e5a76ef --- /dev/null +++ b/app/plugins/wip/views/admin/edit.html @@ -0,0 +1,48 @@ +
    + Edit WIP +
    + {ADMIN_BREADCRUMBS} +
    +
    + +
    + +
    + +
    +
    + + +
    + +
    + +
    +
    + + +
    + +
    + +
    +
    + + +
    + +
    + +
    +
    + + + + + +
    + +
    +
    +
    +
    \ No newline at end of file diff --git a/app/plugins/wip/views/admin/list.html b/app/plugins/wip/views/admin/list.html new file mode 100644 index 0000000..759b29a --- /dev/null +++ b/app/plugins/wip/views/admin/list.html @@ -0,0 +1,44 @@ +
    + Works in Progress +
    + {ADMIN_BREADCRUMBS} +
    + + + + + + + + + + + + + {LOOP} + + + + + + + + + {/LOOP} + {ALT} + + + + {/ALT} + +
    TitleProgressStart Date + +
    {title}{progress}{startDate} + +
    + No results to show. +
    + Create + +
    +
    \ No newline at end of file diff --git a/app/plugins/wip/views/admin/view.html b/app/plugins/wip/views/admin/view.html new file mode 100644 index 0000000..657f93a --- /dev/null +++ b/app/plugins/wip/views/admin/view.html @@ -0,0 +1,46 @@ +
    +
    +
    + {ADMIN_BREADCRUMBS} +
    + +
    +

    {title}

    +
    + + +
    +
    + + + + + + + + + + + + + + + + + + + + +
    Title{title}
    Progress{progress}
    Started:{DTC}{startDate}{/DTC}
    Description{description}
    +
    +
    + + + +
    +
    +
    +
    \ No newline at end of file diff --git a/app/plugins/wip/views/wip.html b/app/plugins/wip/views/wip.html new file mode 100644 index 0000000..57eda97 --- /dev/null +++ b/app/plugins/wip/views/wip.html @@ -0,0 +1,24 @@ +
    +

    Work in Progress

    +
    + {LOOP} +
    +

    {title}

    + Started: {prettyStart} +
    +
    +
    +
    + {description} +
    +
    +
    +
    +
    + {/LOOP} + {ALT} +
    +

    None Found

    +
    + {/ALT} +
    \ No newline at end of file diff --git a/app/resources/config/config.example.json b/app/resources/config/config.example.json deleted file mode 100644 index f8dcbc2..0000000 --- a/app/resources/config/config.example.json +++ /dev/null @@ -1,179 +0,0 @@ -{ - "main": { - "name": { - "type": "text", - "pretty": "Site Name", - "default": "TTP Example", - "value": "TTP Example" - }, - "loginLimit": { - "type": "text", - "pretty": "Maximum Login Attempts per hour", - "default": 5, - "value": 5 - }, - "logo": { - "type": "file", - "pretty": "Site Logo (Used mostly in emails)", - "default": "images/logo.png", - "value": "images/logo.png" - }, - "template": { - "type": "text", - "pretty": "Default Site Template", - "default": "default", - "value": "default" - }, - "tokenEnabled": { - "type": "radio", - "pretty": "Enable CSRF Token for all forms.", - "default": true, - "value": true - } - }, - "uploads": { - "files": { - "type": "radio", - "pretty": "Enable File Uploads", - "default": true, - "value": true - }, - "images": { - "type": "radio", - "pretty": "Enable Image Uploads", - "default": true, - "value": true - }, - "maxFileSize": { - "type": "text", - "pretty": "Maximum File Size", - "default": 5000000, - "value": 5000000 - }, - "maxImageSize": { - "type": "text", - "pretty": "Maximum Image Size", - "default": 500000, - "value": 500000 - } - }, - "database": { - "dbHost": { - "type": "text", - "pretty": "Database Host (IE: http://localhost:3306)", - "default": "127.0.0.1", - "protected": true, - "value": "127.0.0.1" - }, - "dbUsername": { - "type": "text", - "pretty": "Database Username", - "default": "root", - "protected": true, - "value": "root" - }, - "dbPrefix": { - "type": "text", - "pretty": "Database table Prefix", - "default": "TTP_", - "protected": true, - "value": "TTP_" - }, - "dbPassword": { - "type": "text", - "pretty": "Database Password", - "default": "", - "protected": true, - "value": "" - }, - "dbName": { - "type": "text", - "pretty": "Database Name", - "default": "ttp-example", - "protected": true, - "value": "ttp-example" - }, - "dbEnabled": { - "type": "radio", - "pretty": "Database Enabled", - "default": true, - "protected": true, - "value": true - }, - "dbMaxQuery": { - "type": "text", - "pretty": "Maximum results per query", - "default": 100, - "protected": true, - "value": 100 - } - }, - "group": { - "defaultGroup": { - "type": "customSelect", - "pretty": "The Default Group for new registrations.", - "default": 5, - "value": 5 - } - }, - "logging": { - "admin": { - "type": "radio", - "pretty": "Enable Admin Action Logging.", - "default": true, - "value": true - }, - "errors": { - "type": "radio", - "pretty": "Enable Error Logging", - "default": true, - "value": true - }, - "logins": { - "type": "radio", - "pretty": "Enable Login Logging", - "default": true, - "value": true - } - }, - "bugReports": { - "enabled": { - "type": "radio", - "pretty": "Enable Bug reporting.", - "default": true, - "value": true - }, - "sendEmail": { - "type": "radio", - "pretty": "Email the user after submiting.", - "default": true, - "value": true - }, - "emailTemplate": { - "type": "text", - "pretty": "Email Template", - "default": "BugReportEmail", - "value": "BugReportEmail" - } - }, - "feedback": { - "enabled": { - "type": "radio", - "pretty": "Enable User Feedback.", - "default": true, - "value": true - }, - "sendEmail": { - "type": "radio", - "pretty": "Email the user after submiting.", - "default": false, - "value": false - }, - "emailTemplate": { - "type": "text", - "pretty": "Email Template", - "default": "feedbackEmail", - "value": "feedbackEmail" - } - } -} \ No newline at end of file diff --git a/app/resources/config/install.example.json b/app/resources/config/install.example.json deleted file mode 100644 index 8f1b143..0000000 --- a/app/resources/config/install.example.json +++ /dev/null @@ -1,162 +0,0 @@ -{ - "installHash": "014c857a650bcb0e4b08b14e1924287c", - "installStep": "complete", - "modules": { - "Group": { - "name": "Group", - "installDate": 1670000000, - "lastUpdate": 1670000000, - "installStatus": "Installed", - "installedVersion": "3.0", - "folder": "/var/www/app/models/", - "installTable": "Success", - "installPermissions": "Success", - "installConfigs": "Success", - "installResources": "Success", - "installPreferences": "Not Required", - "installedResources": ["1", "2", "3", "4", "5", "6"] - }, - "Log": { - "name": "Log", - "installDate": 1670000000, - "lastUpdate": 1670000000, - "installStatus": "Installed", - "installedVersion": "3.0", - "folder": "/var/www/app/models/", - "installTable": "Success", - "installPermissions": "Not Required", - "installConfigs": "Success", - "installResources": "Not Required", - "installPreferences": "Not Required" - }, - "Message": { - "name": "Message", - "installDate": 1670000000, - "lastUpdate": 1670000000, - "installStatus": "Installed", - "installedVersion": "3.0", - "folder": "/var/www/app/models/", - "installTable": "Success", - "installPermissions": "Success", - "installConfigs": "Not Required", - "installResources": "Not Required", - "installPreferences": "Not Required" - }, - "Routes": { - "name": "Routes", - "installDate": 1670000000, - "lastUpdate": 1670000000, - "installStatus": "Installed", - "installedVersion": "3.0", - "folder": "/var/www/app/models/", - "installTable": "Success", - "installPermissions": "Success", - "installConfigs": "Not Required", - "installResources": "Success", - "installPreferences": "Not Required", - "installedResources": ["1", "2", "3", "4", "5"] - }, - "Sessions": { - "name": "Sessions", - "installDate": 1670000000, - "lastUpdate": 1670000000, - "installStatus": "Installed", - "installedVersion": "3.0", - "folder": "/var/www/app/models/", - "installTable": "Success", - "installPermissions": "Not Required", - "installConfigs": "Not Required", - "installResources": "Not Required", - "installPreferences": "Not Required" - }, - "User": { - "name": "User", - "installDate": 1670000000, - "lastUpdate": 1670000000, - "installStatus": "Installed", - "installedVersion": "3.0", - "folder": "/var/www/app/models/", - "installTable": "Success", - "installPermissions": "Success", - "installConfigs": "Not Required", - "installResources": "Not Required", - "installPreferences": "Success" - }, - "Blog": { - "name": "Blog", - "enabled": true, - "enabled_txt": "yes", - "installDate": 1670000000, - "lastUpdate": 1670000000, - "installStatus": "Installed", - "installedVersion": "3.0", - "folder": "/var/www/app/plugins/", - "installTable": "Success", - "installPermissions": "Not Required", - "installConfigs": "Not Required", - "installResources": "Success", - "installPreferences": "Not Required", - "installedResources": ["1"] - }, - "Bugreport": { - "name": "Bugreport", - "enabled": true, - "enabled_txt": "yes", - "installDate": 1670000000, - "lastUpdate": 1670000000, - "installStatus": "Installed", - "installedVersion": "3.0", - "folder": "/var/www/app/plugins/", - "installTable": "Success", - "installPermissions": "Success", - "installConfigs": "Success", - "installResources": "Not Required", - "installPreferences": "Not Required" - }, - "Comments": { - "name": "Comments", - "enabled": true, - "enabled_txt": "yes", - "installDate": 1670000000, - "lastUpdate": 1670000000, - "installStatus": "Installed", - "installedVersion": "3.0", - "folder": "/var/www/app/plugins/", - "installTable": "Success", - "installPermissions": "Not Required", - "installConfigs": "Not Required", - "installResources": "Not Required", - "installPreferences": "Not Required" - }, - "Feedback": { - "name": "Feedback", - "enabled": true, - "enabled_txt": "yes", - "installDate": 1670000000, - "lastUpdate": 1670000000, - "installStatus": "Installed", - "installedVersion": "3.0", - "folder": "/var/www/app/plugins/", - "installTable": "Success", - "installPermissions": "Success", - "installConfigs": "Success", - "installResources": "Not Required", - "installPreferences": "Not Required" - }, - "Subscribe": { - "name": "Subscribe", - "enabled": true, - "enabled_txt": "yes", - "installDate": 1670000000, - "lastUpdate": 1670000000, - "installStatus": "Installed", - "installedVersion": "3.0", - "folder": "/var/www/app/plugins/", - "installTable": "Success", - "installPermissions": "Not Required", - "installConfigs": "Not Required", - "installResources": "Not Required", - "installPreferences": "Not Required" - } - } -} diff --git a/app/resources/config/permissions.example.json b/app/resources/config/permissions.example.json deleted file mode 100644 index 319f727..0000000 --- a/app/resources/config/permissions.example.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "adminAccess": { - "pretty": "Access Administrator Areas", - "default": false - }, - "modAccess": { - "pretty": "Access Moderator Areas", - "default": false - }, - "memberAccess": { - "pretty": "Access Member Areas", - "default": false - }, - "sendMessages": { - "pretty": "Send messages to other users", - "default": false - }, - "addRoute": { - "pretty": "Add Custom Routes", - "default": false - }, - "uploadImages": { - "pretty": "Upload images (such as avatars)", - "default": false - }, - "bugReport": { - "pretty": "Can Submit Bug Reports", - "default": false - }, - "feedback": { - "pretty": "Can Submit Feedback", - "default": false - } -} \ No newline at end of file diff --git a/app/resources/config/prefrences.example.json b/app/resources/config/prefrences.example.json deleted file mode 100644 index 2b10ade..0000000 --- a/app/resources/config/prefrences.example.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "gender": { - "pretty": "Gender", - "type": "select", - "default": "unspecified", - "options": ["male", "female", "other", "unspecified"], - "avatar": "/var/www/apache/images/defaultAvatar.png" - }, - "email": { - "pretty": "IDK what this is for", - "type": "checkbox", - "default": "true", - "avatar": "/var/www/apache/images/defaultAvatar.png", - "options": null - }, - "newsletter": { - "pretty": "Receive our Newsletter?", - "type": "checkbox", - "default": "true", - "avatar": "/var/www/apache/images/defaultAvatar.png", - "options": null - }, - "avatar": { - "pretty": "Avatar", - "type": "file", - "default": "images/defaultAvatar.png", - "avatar": "/var/www/apache/images/defaultAvatar.png", - "options": null - }, - "timezone": { - "pretty": "Timezone", - "type": "timezone", - "default": "America/New_York", - "avatar": "/var/www/apache/images/defaultAvatar.png", - "options": null - }, - "dateFormat": { - "pretty": "Date Format", - "type": "select", - "default": "F j, Y", - "options": { - "1-8-1991": "n-j-Y", - "8-1-1991": "j-n-Y", - "01-08-1991": "m-d-Y", - "08-01-1991": "d-m-Y", - "January 8, 1991": "F-j-Y", - "8 January, 1991": "j-F-Y", - "January 08, 1991": "F-d-Y", - "08 January, 1991": "d-F-Y", - "Jan 8, 1991": "M-j-Y", - "8 Jan 1991": "j-M-Y", - "Jan 08, 1991": "M-d-Y", - "08 Jan 1991": "d-M-Y" - }, - "avatar": "/var/www/apache/images/defaultAvatar.png" - }, - "timeFormat": { - "pretty": "Time Format", - "type": "select", - "default": "g:i:s A", - "options": { - "3:33:33 AM": "g:i:s A", - "03:33:33 AM": "h:i:s A", - "3:33:33 am": "g:i:s a", - "03:33:33 am": "h:i:s a", - "3:33:33 (military)": "G:i:s", - "03:33:33 (military)": "H:i:s" - }, - "avatar": "/var/www/apache/images/defaultAvatar.png" - }, - "pageLimit": { - "pretty": "Items Displayed Per Page", - "type": "select", - "default": "10", - "options": ["10", "15", "20", "25", "50"], - "avatar": "/var/www/apache/images/defaultAvatar.png" - } -} diff --git a/app/resources/controllers/example.php b/app/resources/controllers/example.php deleted file mode 100644 index d813ed5..0000000 --- a/app/resources/controllers/example.php +++ /dev/null @@ -1,54 +0,0 @@ - - * @link https://TheTempusProject.com - * @license https://opensource.org/licenses/MIT [MIT LICENSE] - */ -namespace TheTempusProject\Controllers; - -use TheTempusProject\Classes\Controller; -use TheTempusProject\Houdini\Classes\Template; -use TheTempusProject\Houdini\Classes\Views; -use TheTempusProject\Canary\Bin\Canary as Debug; - -// The new controller must extend the default controller class or some functionality will be lost. -class Example extends Controller { - /** - * A constructor does not have to be defined. - * If a constructor is used, it must call the parent constructor first. - */ - public function __construct() { - parent::__construct(); - Template::noFollow(); - Template::noIndex(); - Template::setTemplate( 'example' ); - } - - /** - * A destructor does not have to be defined. - * If a destructor is used, it must call the parent destructor last. - */ - public function __destruct() { - Debug::log( 'Something to log before the app ends execution.' ); - parent::__destruct(); - } - - public function index() { - self::$title = 'Example Controller'; - Issues::add( 'error', [ 'This is an error with multiple parts.' => [ 'Error 1', 'Error 2' ] ] ); - Issues::add( 'error', 'This is a single error.' ); - Issues::add( 'success', [ 'This is a success with multiple parts.' => [ 'Success 1', 'Success 2' ] ] ); - Issues::add( 'success', 'This is a single success.' ); - Issues::add( 'notice', 'This is a single notice.' ); - Issues::add( 'info', 'This is a single info.' ); - Components::set( 'simple', Views::simpleView( 'simple' ) ); - Components::set( 'complex', Views::simpleView( 'complex' ) ); - Views::view( 'example' ); - $example = Views::simpleView( 'example' ); - } -} diff --git a/app/resources/nginx-main.conf b/app/resources/nginx-main.conf deleted file mode 100644 index db9385d..0000000 --- a/app/resources/nginx-main.conf +++ /dev/null @@ -1,44 +0,0 @@ -server { - listen 8080 default_server; - listen [::]:8080 default_server; - listen 8081 ssl default_server; - listen [::]:8081 ssl default_server; - - ssl_prefer_server_ciphers on; - - include snippets/common.conf; - - access_log /var/log/nginx/access.log; - error_log /var/log/nginx/error.log; - - location ~* \.(?:js|css|png|jpg|gif|ico)$ { - access_log off; - log_not_found off; - } - - location /js/ { - access_log off; - log_not_found off; - try_files $uri /index.php?error=js404&file=$uri; - } - - location /css/ { - access_log off; - log_not_found off; - try_files $uri /index.php?error=css404&file=$uri; - } - - location / { - if (!-e $request_filename){ - rewrite ^/images/(.*)$ /index.php?error=image404&url=$1 break; - rewrite ^/uploads/(.*)$ /index.php?error=upload404&url=$1 break; - } - rewrite ^/errors/(.*)$ /index.php?error=$1 break; - rewrite ^/(.+)$ /index.php?url=$1&$args; - } - - location ~ \.php$ { - fastcgi_pass unix:/var/run/php/php8.2-fpm.sock; - include snippets/fastcgi-php.conf; - } -} \ No newline at end of file diff --git a/app/resources/nodels/example.php b/app/resources/nodels/example.php deleted file mode 100644 index 5278442..0000000 --- a/app/resources/nodels/example.php +++ /dev/null @@ -1,20 +0,0 @@ - - * @link https://TheTempusProject.com - * @license https://opensource.org/licenses/MIT [MIT LICENSE] - */ -namespace TheTempusProject\Models; - -use TheTempusProject\Bedrock\Classes\Model; - -class Example extends Model { - public function __construct() { - parent::__construct(); - } -} diff --git a/app/resources/plugin.php b/app/resources/plugin.php deleted file mode 100644 index 4947374..0000000 --- a/app/resources/plugin.php +++ /dev/null @@ -1,99 +0,0 @@ - - * @link https://TheTempusProject.com - * @license https://opensource.org/licenses/MIT [MIT LICENSE] - */ -namespace TheTempusProject\Plugins; - -use ReflectionClass; -use TheTempusProject\Classes\Installer; -use TheTempusProject\Houdini\Navigation; -use TheTempusProject\Models\forealthough as forealthoughModel; -use TheTempusProject\TheTempusProject as App; - -class notrealplugin extends forealthoughModel { - public static $initialized; - public $pluginName = 'TP XXXXXXXXXX'; - public $pluginAuthor = 'JoeyK'; - public $pluginWebsite = 'https://TheTempusProject.com'; - public $modelVersion = '1.0'; - public $pluginVersion = '1.0'; - public $pluginDescription = 'A simple plugin which adds a site wide XXXXXXXXXX system.'; - public $configName = 'XXXXXXXXXX'; - public $databaseMatrix = [ - [ 'title', 'varchar', '86' ], - [ 'suggestion', 'text', '' ], - [ 'suggestedOn', 'int', '10' ], - [ 'approved', 'varchar', '5' ], - [ 'approvedOn', 'int', '10' ], - [ 'approvedBy', 'int', '11' ], - [ 'author', 'int', '11' ], - ]; - public $configMatrix = [ - 'enabled' => [ - 'type' => 'radio', - 'pretty' => 'Enable XXXXXXXXXX.', - 'default' => true, - ], - ]; - public $permissionMatrix = [ - 'XXXXXXXXXX' => [ - 'pretty' => 'Can create XXXXXXXXXX', - 'default' => false, - ], - ]; - public $admin_links = [ - [ - 'text' => ' Suggestions', - 'url' => '{ROOT_URL}admin/suggestions', - ], - ]; - public $main_links = [ - [ - 'text' => 'Suggestions', - 'url' => '{ROOT_URL}suggestions/index', - ], - ]; - public $resourceMatrix = [ - [ - 'title' => 'Welcome', - 'content' => '

    This is just a simple message to say thank you for installing The Tempus Project. If you have any questions you can find everything through our website here.

    ', - 'author' => 1, - 'created{time}' => 0, - 'edited{time}' => 0, - 'draft' => 0, - ], - ]; - public $contact_footer_links = [ - [ - 'text' => 'Bug Report', - 'url' => '{ROOT_URL}bugreport', - ], - ]; - public function __construct() { - $reflect = new ReflectionClass( $this ); - if ( true === self::$initialized || !Installer::pluginEnabled( $reflect->getShortName() ) ) { - return; - } - foreach ( $this->contact_footer_links as $key => $link ) { - Navigation::addLink( App::CONTACT_FOOTER_MENU_NAME, $link ); - } - foreach ( $this->info_footer_links as $key => $link ) { - Navigation::addLink( App::INFO_MENU_NAME, $link ); - } - foreach ( $this->main_links as $key => $link ) { - Navigation::addLink( App::MAIN_MENU_NAME, $link ); - } - foreach ( $this->admin_links as $key => $link ) { - Navigation::addLink( App::ADMIN_MENU_NAME, $link ); - } - self::$initialized = true; - } -} diff --git a/app/resources/templates/example.tpl b/app/resources/templates/example.tpl deleted file mode 100644 index 0769ead..0000000 --- a/app/resources/templates/example.tpl +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - - {TITLE} - - - - {ROBOT} - - - - - - - - - - -
    -
    - {ISSUES} -
    -
    - {ERROR} - {NOTICE} - {SUCCESS} - {INFO} -
    -
    - {/ISSUES} -
    -
    - {CONTENT} -
    -
    -
    -
    -
    - {FOOT} - {COPY} -
    -
    -
    - - - - - \ No newline at end of file diff --git a/app/resources/templates/exanple.inc.php b/app/resources/templates/exanple.inc.php deleted file mode 100644 index b6cdb2d..0000000 --- a/app/resources/templates/exanple.inc.php +++ /dev/null @@ -1,34 +0,0 @@ - - * @link https://TheTempusProject.com - * @license https://opensource.org/licenses/MIT [MIT LICENSE] - */ -namespace TheTempusProject\Templates; - -use TheTempusProject\Houdini\Classes\Views; -use TheTempusProject\Houdini\Classes\Navigation; -use TheTempusProject\Houdini\Classes\Components; -use TheTempusProject\Bedrock\Classes\Config; -use TheTempusProject\TheTempusProject as App; - -class ExampleLoader { - public function __construct() { - Components::set( 'TEMPLATE_URL', Template::parse( '{ROOT_URL}app/templates/default/' ) ); - Components::set( 'LOGO', Config::get( 'main/logo' ) ); - Components::set( 'FOOT', Views::simpleView( 'foot' ) ); - Components::set( 'COPY', Views::simpleView( 'copy' ) ); - if ( App::$isLoggedIn ) { - Components::set( 'STATUS', Views::simpleView( 'nav.statusLoggedIn' ) ); - Components::set( 'USERNAME', App::$activeUser->username ); - } else { - Components::set( 'STATUS', Views::simpleView( 'nav.statusLoggedOut' ) ); - } - Components::set( 'topNavLeft', Navigation::activePageSelect( 'nav.main' ) ); - } -} diff --git a/app/resources/views/complex.html b/app/resources/views/complex.html deleted file mode 100644 index f20720e..0000000 --- a/app/resources/views/complex.html +++ /dev/null @@ -1,3 +0,0 @@ -

    This is a more complex component that includes another component...

    -{simple} -

    ...right in the middle!

    \ No newline at end of file diff --git a/app/resources/views/example.html b/app/resources/views/example.html deleted file mode 100644 index e546b1e..0000000 --- a/app/resources/views/example.html +++ /dev/null @@ -1,141 +0,0 @@ - -

    It is comprised entirely of free form HTML

    -

    If you are feeling extra bold you can use the templating engine to set variables to be replaced at runtime such as {variable} or even {variable2}, or maybe even just look through some data:

    -{loop} -{value1} is the first value.
    -{value2} is the second value.
    -{value3} is the third value.
    -{/loop} -{ALT}No Loop{/ALT} -{footer} - - - - - - - - -

    This is a default view

    - -

    The Tempus-Project template-processor works with the backend to render html for the end user. This process has several steps and components that can get pretty complex. For the moment let's just review a few that can be used to generate web pages.

    - -

    Views

    -

    Views are the most basic interaction between the front-end and back-end. Inside of a controler, you can call a view in two ways:

    -

    1. Normal - Views::view

    -
    
    -

    2. Inline - Views::simpleView

    -
    
    -
    -
    -

    Components

    -

    You can think of components as a sort of front-end variable that can be filled in by the back-end before being send to the end-user. You can include components in all template parsing with a simple command:

    -
    
    -Adding this to a controler will give access to that component's value in the rendering engine. For example:
    -
    
    -
    -

    Pagination

    -PAGINATION - if (Pagination::totalPages() <= 1) { - Components::set('PAGINATION', ''); - } else { - Components::set('PAGINATION', Views::simpleView('nav.pagination', $pageData)); - } - - - -

    Navigation

    - -

    Filters

    -

    In some cases, you may want to hide or show text on a page conditionally. For example, you may have administrator controls on a commonly used page. Obviously you would like to hide those controls from regular users; even if you have safeguards to prevent them from performing any restricted actions.

    -

    This is where filters come in. They do exactly that, conditionally hide or show part of a page based on back-end logic. The admin example is so common, its already built in. If a user has the isAdmin permission on thier group, they will be able to see anything within the "ADMIN" tag:

    -
    -    Filters::add('member', '#{MEMBER}(.*?){/MEMBER}#is', (self::$isMember ? '$1' : ''), true);
    -    Filters::add('mod', '#{MOD}(.*?){/MOD}#is', (self::$isMod ? '$1' : ''), true);
    -    Filters::add('admin', '#{ADMIN}(.*?){/ADMIN}#is', (self::$isAdmin ? '$1' : ''), true);
    -
    - - - - - - - - '#\[b\](.*?)\[/b\]#is' => '$1', - '#\[p\](.*?)\[/p\]#is' => '

    $1

    ', - '#\[i\](.*?)\[/i\]#is' => '$1', - '#\[u\](.*?)\[/u\]#is' => '$1', - '#\[s\](.*?)\[/s\]#is' => '$1', - '#\[code\](.*?)\[/code\]#is' => '$1', - '#\[color=(.*?)\](.*?)\[/color\]#is' => "$2", - '#\[img\](.*?)\[/img\]#is' => "", - '#\[url=(.*?)\](.*?)\[/url\]#is' => "$2", - '#\[quote=(.*?)\](.*?)\[/quote\]#is' => "
    $2
    ", - '#\(c\)#is' => '✔', - '#\(x\)#is' => '✖', - '#\(!\)#is' => '❕', - '#\(\?\)#is' => '❔', - '#\[list\](.*?)\[/list\]#is' => '
      $1
    ', - '#\(\.\)(.*)$#m' => '
  • $1
  • ', - '/(^|\s)@(\w*[a-zA-Z_]+\w*)/' => ' @\2', - '/(^|\s)#(\w*[a-zA-Z_]+\w*)/' => ' #\2', - '#/\*.*?\*/#s' => null, - '#(? null, - "#{CHECKED:(.*?)=(.*?)}#s" => null, - - - - - - - - - - - - - - - - - - - -

    Issues

    -

    One of the pre-existing filters happens to be Issues. In the controller for this file, you should see a block that includes several examples of Issues. These issues are automatically added as individual components and hidden with the issues filter.

    -

    -    Issues::add( 'error', [ 'This is an error with multiple parts.' => [ 'Error 1', 'Error 2' ] ] );
    -    Issues::add( 'error', 'This is a single error.' );
    -    Issues::add( 'success', [ 'This is a success with multiple parts.' => [ 'Success 1', 'Success 2' ] ] );
    -    Issues::add( 'success', 'This is a single success.' );
    -    Issues::add( 'notice', 'This is a single notice.' );
    -    Issues::add( 'info', 'This is a single info.' );
    -    Filters::add('issues', '#{ISSUES}(.*?){/ISSUES}#is', (Issues::hasIssues() ? '$1' : ''), true);
    -    Components::set( 'NOTICE', $test );
    -    Components::set( 'SUCCESS', $test );
    -    Components::set( 'ERROR', $test );
    -    Components::set( 'INFO', $test );
    -
    - - - - - - - - - - - - - - - -

    Forms

    -select Radio - $selected = 'CHECKED:' . $fieldName . '=' . $value; - Components::set($selected, 'checked="checked"'); -select Option - $find = "#\