From 46d681c6b825d21b3f698d793c4e630c687d90ad Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Thu, 21 May 2026 21:41:53 +0000
Subject: [PATCH] =Major CustomBlocks.php overhaul, expanding block support and customization from the editor. theme.json should now be updated on new themes to set brand colours, etc. Also note: major change to .col vs .row alignment: simplifying it to .top .bottom vs the confusion of the differences for .col/.row .start and .a-start

---
 inc/managers/LoginManager.php | 1917 +++++++++++++++++++++++++++++------------------------------
 1 files changed, 944 insertions(+), 973 deletions(-)

diff --git a/inc/managers/LoginManager.php b/inc/managers/LoginManager.php
index 7b04aef..68db551 100644
--- a/inc/managers/LoginManager.php
+++ b/inc/managers/LoginManager.php
@@ -1,1054 +1,1025 @@
 <?php
 namespace JVBase\managers;
 
-use JVBase\meta\MetaManager;
-use WP_Error;
+use JVBase\base\Site;
+use JVBase\forms\TaxonomySelector;
+use JVBase\meta\Form;
+
+use JVBase\registrar\Registrar;use WP_Error;
 use WP_User;
 
 if (!defined('ABSPATH')) {
-    exit; // Exit if accessed directly
+	exit;
 }
 
 class LoginManager
 {
-    private array|null $invitation_data = null;
-    protected array $inviteData = [];
-    private array $allowed_file_types = [
-        'image/jpeg',
-        'image/png',
-        'image/gif',
-        'application/pdf'
-    ];
-    private int $max_file_size = 5242880; // 5MB in bytes
+	protected ?Form $form = null;
+	protected Cache $cache;
 
-    public function __construct()
-    {
-        // Common login page customization
-        add_action('login_enqueue_scripts', array($this, 'loginStyles'));
-        add_action('login_header', array($this, 'loginHeader'), 0);
-        add_action('login_footer', array($this, 'loginFooter'));
 
-        // Login page filters
-        add_filter('login_headerurl', array($this, 'logoUrl'));
-        add_filter('login_headertext', array($this, 'logoTitle'));
-        add_filter('login_message', array($this, 'loginMessage'));
-        add_filter('login_errors', array($this, 'loginErrors'));
+	protected array $forms =[];
+	protected array $labels = [];
+	protected array $fields = [];
+	protected ?string $action = null;
+	protected string $title = '';
+	protected static LoginManager $instance;
 
-        // Login success handling
-        add_action('wp_login', [$this, 'handleSuccessfulLogin'], 10, 2);
+	// Token handlers registry
+	protected array $messageHandlers = [];
 
-        // Registration-specific hooks
-        if ($this->isRegistrationPage()) {
-            $this->initRegistrationHooks();
-        }
-    }
+	private array $allowed_file_types = [
+		'image/jpeg',
+		'image/png',
+		'image/gif',
+		'application/pdf'
+	];
+	private int $max_file_size = 5242880; // 5MB in bytes
 
-    /**
-     * Check if we're on the registration page
-     */
-    private function isRegistrationPage(): bool
-    {
-        return isset($_GET['action']) && $_GET['action'] === 'register';
-    }
 
-    /**
-     * Initialize registration-specific hooks
-     */
-    private function initRegistrationHooks(): void
-    {
-        add_action('register_form', array($this, 'addRegistrationFields'));
-        add_action('login_header', array($this, 'addRegistrationScript'));
-        add_filter('registration_errors', array($this, 'registrationErrorsFilter'), 10, 3);
-        add_action('user_register', array($this, 'saveRegistrationFields'), 999, 2);
-        add_action('login_head', array($this, 'modifyRegistrationForm'));
-        add_action('register_form', array($this, 'addUploadSupport'));
-        add_filter('pre_user_login', array($this, 'setUserLogin'), 1);
-        add_filter('pre_user_email', array($this, 'setUserEmail'), 1);
-        add_filter('register_message', array($this, 'customRegisterMessage'));
-        add_filter('wp_login_errors', array($this, 'registrationSuccessMessage'), 10, 2);
-        add_filter('login_form_top', array($this, 'loginFormTop'));
-        add_filter('login_form_bottom', array($this, 'loginFormBottom'));
-        add_filter('login_form_middle', array($this, 'loginFormMiddle'));
+	public function __construct()
+	{
+		self::$instance = $this;
+		$this->cache = Cache::for('login');
+		$this->cache->flush();
+		// Initialize magic link support if enabled
+		if (Site::has('magicLink')) {
+			$this->initMagicLinkSupport();
+		}
 
-        // Remove default username requirement for registration
-        remove_filter('registration_errors', 'registration_auth_pass_filter', 10);
-    }
+		// Create login page if it doesn't exist
+		$this->ensureLoginPageExists();
 
-    /**
-     * Combined login styles for both login and registration
-     */
-    public function loginStyles(): void
-    {
-        do_action('jvbLoginStyles');
-    }
 
-    /**
-     * Login header - used for both login and registration
-     */
-    public function loginHeader(): void
-    {
-        ?>
-        <script type="text/javascript">
-            document.addEventListener('DOMContentLoaded', function() {
-                let loginLabel = document.querySelector('label[for="user_login"');
-                loginLabel.innerHTML = '<?= jvbIcon('email', ['size' => 20]); ?> Your Email';
+		// Redirect wp-login.php to custom page
+		add_action('login_init', [$this, 'redirectToCustomLogin']);
+		add_action('template_include', [$this, 'renderLoginPage']);
 
-                let passwordLabel = document.querySelector('label[for="user_pass"');
-                passwordLabel.innerHTML = '<?= jvbIcon('password', ['size' => 20]); ?> Your Password';
+        add_action('wp_enqueue_scripts', [$this, 'enqueueScripts'], 15);
 
-                document.querySelector('form').classList.add('loaded');
-            });
+		// Login success handling
+		add_action('wp_login', [$this, 'handleSuccessfulLogin'], 10, 2);
 
-        </script>
-        <?php
-    }
+		add_filter('lostpassword_url', [$this, 'resetPasswordUrl'], 10, 2);
+		add_filter( 'login_url', [$this, 'loginUrl'], 10, 3 );
+		add_filter( 'logout_url', [$this, 'logoutUrl'], 10, 2 );
+		// Allow other features to register handlers
+		do_action('jvbLoginManagerInit', $this);
+		add_action('user_register', array($this, 'saveRegistrationFields'), 999, 2);
+		add_filter('the_seo_framework_sitemap_exclude_ids', [$this, 'excludeLoginSitemap'], 10, 1);
+	}
+	public static function getInstance():self
+	{
+		return self::$instance;
+	}
 
-    /**
-     * Login footer with donate section
-     */
-    public function loginFooter(): void
-    {
-		do_action('jvbLoginFooter');
+	public function excludeLoginSitemap(array $ids): array
+	{
+		$ids[] = $this->getLoginPage();
+		return $ids;
+	}
+	/**************************************************************************
+	   * SETUP & CONFIGURATION
+	**************************************************************************/
 
-    }
+	/**
+	 * Redirect wp-login.php to custom login page
+	 */
+	public function redirectToCustomLogin(): void
+	{
+		// Handle interim login
+		if (isset($_GET['interim-login'])) {
+			// Don't redirect - let WP handle it
+			return;
+		}
+		// Don't redirect if AJAX or REST
+		if ((defined('DOING_AJAX') && DOING_AJAX) || (defined('REST_REQUEST') && REST_REQUEST)) {
+			return;
+		}
+		// Build custom login URL with all query args
+		$custom_login_page = home_url('/login/');
+		$query_args = $_GET;
 
-    /**
-     * Logo URL
-     */
-    public function logoUrl(): string
-    {
-        return home_url();
-    }
+		// Remove WordPress internal args
+		unset($query_args['interim-login'], $query_args['wp-auth-check']);
 
-    /**
-     * Logo title
-     */
-    public function logoTitle(): string
-    {
-        return get_bloginfo('name');
-    }
+		if (!empty($query_args)) {
+			$custom_login_page = add_query_arg($query_args, $custom_login_page);
+		}
 
-    /**
-     * Login message - handles both login and registration
-     */
-    public function loginMessage(string $message): string
-    {
-        if ($this->isRegistrationPage()) {
-            if (jvbSiteHasInvitations() && $this->fromInvite()) {
-                $data = JVB()->routes('invites')->verifyInvitation(sanitize_text_field($_GET['invite']), sanitize_email($_GET['email']));
-                $name = $data->name;
-                $inviters = json_decode($data->inviters, true);
-                $names = [];
-                foreach ($inviters as $inviter) {
-                    $artist = jvbContentFromUser((int)$inviter['user_id']);
-                    $names[] = ($artist['name'] === '') ? $artist['display_name'] : $artist['name'];
-                }
-                $message = (count($names) > 1) ? 'are already here, and have invited you to join in!' : ' is already here, and invited you to join in!';
-                return '<h2>Join the Scene, '.$name.'</h2>
-                <p style="text-align:center;">'.jvbCommaList($names).$message.'</p>';
-            }
-            if (jvbSiteHasFavourites() && $this->fromFavourites()) {
-                return '<h2>'.JVB_LOGIN['login_from_favourite_header']??'Save your Favourites'.'</h2>';
-            }
-            return '<h2>'.JVB_LOGIN['join_header'].'</h2>';
-        } else {
-			if (jvbSiteHasFavourites()) {
-				$login = (!$this->fromFavourites()) ? '<h2>'.JVB_LOGIN['login_header'].'</h2>' : '<h2>'.JVB_LOGIN['login_from_favourite_header'].'</h2>';
-			} else {
-				$login = '<h2>'.JVB_LOGIN['login_header'].'</h2>';
+		wp_safe_redirect($custom_login_page);
+		exit;
+	}
+	protected function getRegistrationFormFields():array
+	{
+		$form = get_option(BASE.'registration_form_fields');
+		if (!$form) {
+			$form = [];
+
+			$select = [];
+			//Basic fields, for any
+			$fields = [
+				'user_name'	=> [
+					'type'		=> 'text',
+					'required'	=> true,
+					'label'		=> 'Your Name',
+					'placeholder'=> 'Mister Meseeks'
+				],
+				'user_email'	=> [
+					'type'		=> 'email',
+					'required'	=> true,
+					'label'		=> 'Your Email',
+					'placeholder'=> 'look@me.com'
+				]
+			];
+			if (Site::has('referrals')) {
+				$fields['referral_code'] = [
+					'type'	=> 'text',
+					'required'=> false,
+					'label'	=> 'Referral Code',
+					'hint'	=> 'Have a referral code? Paste it here!'
+				];
+			}
+			$canRegister = Registrar::getFeatured('can_register', 'user');
+			if (!empty($canRegister)) {
+				foreach ($canRegister as $role) {
+					$registrar = Registrar::getInstance($role);
+					$config = $registrar->getConfig('register');
+					$icon = $registrar->getIcon('user');
+					$icon = ($icon !== '') ? jvbIcon($icon) : '';
+					$select[$role] = '<span class="label">'.$icon.$registrar->getSingular().'</span><span class="text">'.$config['description']??Site::login()->getDescription('register')??''.'</span>';
+					if (!empty($config['fields'])){
+						foreach ($config['fields'] as $field) {
+							$field['condition'] = [
+								'field'	=> 'user_select',
+								'value'	=> $role,
+								'operator'	=> '=='
+							];
+							$fields[] = $field;
+						}
+					}
+				}
+				if (!empty($select)) {
+					$select = array_merge(
+						[
+							'subscriber'	=> 'Subscriber',
+						],
+						$select
+					);
+					$form = array_merge(
+						[
+							'user_select' => [
+								'type'	=> 'radio',
+								'label'	=> 'Register as',
+								'options'	=> $select,
+								'required'	=> true,
+								'default'	=> 'subscriber'
+							]
+						],
+						$fields
+					);
+				}
+			}else {
+				$form = $fields;
+			}
+			update_option(BASE.'registration_form_fields', $form);
+		}
+		return $form;
+
+	}
+
+	protected function setupFields():void
+	{
+		$this->fields = $this->getFieldsForAction($this->action);
+	}
+
+	protected function getFieldsForAction(string $action):array
+	{
+		$fields = [];
+		switch($action) {
+			case 'register':
+				$fields = $this->getRegistrationFormFields();
+				break;
+			case 'lostpassword':
+			case 'magic':
+				$fields = [
+					'user_email' => [
+						'type' => 'email',
+						'label' => __('Email Address', 'jvb'),
+						'required' => true,
+						'placeholder' => 'look@me.com',
+					],
+				];
+				break;
+			case 'rp':
+			case 'resetpass':
+				$fields = [
+					'pass1' => [
+						'type' => 'text',
+						'subtype' => 'password',
+						'label' => __('New Password', 'jvb'),
+						'required' => true,
+					],
+					'pass2' => [
+						'type' => 'text',
+						'subtype' => 'password',
+						'label' => __('Confirm Password', 'jvb'),
+						'required' => true,
+					],
+				];
+				break;
+			case 'login':
+				$fields = [
+					'user_email' => [
+						'type' => 'email',
+						'label' => __('Email Address', 'jvb'),
+						'required' => true,
+						'autocomplete'	=> 'email',
+						'placeholder' => 'look@me.com',
+					],
+					'user_password' => [
+						'type' => 'text',
+						'subtype'=> 'password',
+						'label' => __('Password', 'jvb'),
+						'autocomplete' => 'current-password',
+						'required' => true,
+					],
+					'remember_me' => [
+						'type' => 'true_false',
+						'label' => __('Remember Me', 'jvb'),
+						'default' => true
+					]
+				];
+				break;
+			case 'postpass':
+				$fields = [
+                'post_password' => [
+                    'type' => 'text',
+                    'subtype' => 'password',
+                    'label' => __('Password', 'jvb'),
+                    'required' => true,
+                    'hint'	=> 'This post is password protected. Please enter the password to view it.',
+                ],
+            ];
+				break;
+			case 'confirmaction':
+
+				break;
+
+		}
+		return $fields;
+	}
+
+
+	/**
+	 * Ensure login page exists
+	 */
+	protected function ensureLoginPageExists(): void
+	{
+		$login_page = $this->getLoginPage();
+
+		if (!$login_page || !is_int($login_page)) {
+			$page_id = get_page_by_path('login');
+			if (!$page_id) {
+				$page_id = wp_insert_post([
+					'post_title' => 'Login',
+					'post_name' => 'login',
+					'post_content' => '[jvb_login_form]',
+					'post_status' => 'publish',
+					'post_type' => 'page',
+					'post_author' => 1
+				]);
 			}
 
-            return (empty($message)) ? $login : $login.$message;
-        }
-    }
-
-	protected function fromFavourites():bool
+			if ($page_id && !is_wp_error($page_id)) {
+				if (is_object($page_id)) {
+					$page_id = (int)$page_id->ID;
+				}
+				update_option(BASE.'login_page', $page_id);
+				// Hide from menus/search
+				update_post_meta($page_id, '_wp_page_template', 'default');
+				update_post_meta($page_id, BASE . 'exclude_from_search', true);
+			}
+		}
+	}
+	public function loginUrl(string $login_url, ?string $redirect, bool $force_reauth):string
 	{
-		return array_key_exists('type', $_GET) && $_GET['type'] === 'favourites';
+		// This will append /custom-login/ to you main site URL as configured in general settings (ie https://domain.com/custom-login/)
+		$login_url = site_url( '/login/', 'login' );
+		if ( ! empty( $redirect ) ) {
+			$login_url = add_query_arg( 'redirect_to', urlencode( $redirect ), $login_url );
+		}
+		if ( $force_reauth ) {
+			$login_url = add_query_arg( 'reauth', '1', $login_url );
+		}
+		return $login_url;
 	}
 
-    /**
-     * Customize login error messages
-     */
-    public function loginErrors(string $error): string
-    {
-        return str_replace(
-            array(
-                'The password you entered for the username',
-                'Invalid username',
-                'Unknown username',
-                'Unknown email address'
-            ),
-            array(
-                'Wrong password',
-                'We can\'t find that username',
-                'We can\'t find that username',
-                'We can\'t find that email'
-            ),
-            $error
-        );
-    }
+	public function logoutUrl(string $logout_url, string $redirect): string
+	{
+		// Build custom logout URL
+		$logout_url = site_url('/login/', 'login');
+		$logout_url = add_query_arg('action', 'logout', $logout_url);
 
-    /**
-     * Handle successful login
-     */
-    public function handleSuccessfulLogin(string $username, WP_User $user): void
-    {
-        if (isOurPeople() && !user_can($user, 'manage_options')) {
-            wp_redirect(get_home_url(2, '/dash'));
-            exit;
-        }
-    }
-
-    // ===== REGISTRATION-SPECIFIC METHODS =====
-
-    /**
-     * Set user login for registration
-     */
-    public function setUserLogin(string $login): string
-    {
-        $user_type = isset($_POST['user_type']) ? $_POST['user_type'] : '';
-        if (!empty($user_type)) {
-            $email_field = $user_type . '_email';
-            if (isset($_POST[$email_field])) {
-                $email = sanitize_email($_POST[$email_field]);
-                if (is_email($email)) {
-                    return $email;
-                }
-            }
-        }
-        return $login;
-    }
-
-    /**
-     * Set user email for registration
-     */
-    public function setUserEmail(string $email): string
-    {
-        $user_type = isset($_POST['user_type']) ? $_POST['user_type'] : '';
-        if (!empty($user_type)) {
-            $email_field = $user_type . '_email';
-            if (isset($_POST[$email_field])) {
-                $email = sanitize_email($_POST[$email_field]);
-                if (is_email($email)) {
-                    return $email;
-                }
-            }
-        }
-        return $email;
-    }
-
-    /**
-     * Modify registration form
-     */
-    public function modifyRegistrationForm(): void
-    {
-        if (!$this->isRegistrationPage()) {
-            return;
-        }
-
-        ?>
-        <script type="text/javascript">
-            document.addEventListener('DOMContentLoaded', function() {
-                // Hide default fields
-                const defaultFields = document.getElementById('registerform').querySelectorAll('p');
-                defaultFields.forEach(field => {
-                    if (field.querySelector('label[for="user_login"]') ||
-                        field.querySelector('label[for="user_email"]')) {
-                        field.remove();
-                    }
-                });
-
-                // Hide the default registration info text
-                const regInfo = document.querySelector('.message.register');
-                if (regInfo) {
-                    regInfo.style.display = 'none';
-                }
-
-                <?php
-                if ($this->fromInvite()) {
-                    $this->handleArtistInvitation();
-                }
-                ?>
-
-                // Move submit button to the end of the form
-                const submitButton = document.getElementById('registerform').querySelector('.submit');
-                if (submitButton) {
-                    document.getElementById('registerform').appendChild(submitButton);
-                }
-            });
-        </script>
-        <?php
-    }
-
-    /**
-     * Handle artist invitation pre-fill
-     */
-    protected function handleArtistInvitation(): void
-    {
-        $token = sanitize_text_field($_GET['invite']);
-        $email = sanitize_email($_GET['email']);
-        $data = JVB()->routes('invites')->verifyInvitation($token, $email);
-
-        ?>
-        document.querySelector('input#artist').checked = true;
-        document.querySelector('#artist_first_name').value = '<?=$data->name?>';
-        document.querySelector('#artist_email').value = '<?=$email?>';
-        <?php
-        if ($data->to_shop) {
-            ?>
-            document.querySelector('#artist_shop').value = '<?=$data->shop?>';
-            <?php
-        }
-        ?>
-        let form = document.getElementById('registerform')
-        let input = document.createElement('input');
-        let email = input.cloneNode(true);
-        input.type = 'hidden';
-        input.name = 'invite_token';
-        input.value = '<?= $token ?>';
-        email.type = 'hidden';
-        email.name = 'invite_email';
-        email.value = '<?= $email?>';
-        form.append(input);
-        form.append(email);
-        <?php
-    }
-
-    /**
-     * Add upload support for registration
-     */
-    public function addUploadSupport(): void
-    {
-        ?>
-        <script>
-            document.addEventListener('DOMContentLoaded', function() {
-                const form = document.getElementById('registerform');
-                if (form) {
-                    form.enctype = 'multipart/form-data';
-                }
-            });
-        </script>
-        <?php
-    }
-
-    /**
-     * Add registration script
-     */
-    public function addRegistrationScript(): void
-    {
-        if (!$this->isRegistrationPage()) {
-            return;
-        }
-        ?>
-        <script>
-            document.addEventListener('DOMContentLoaded', function() {
-
-                // Initialize user type selection
-                function initUserTypeSelection() {
-                    const userTypeRadios = document.querySelectorAll('input[name="user_type"]');
-                    const fieldGroups = document.querySelectorAll('.field-group');
-
-                    userTypeRadios.forEach(radio => {
-                        radio.addEventListener('change', function() {
-                            fieldGroups.forEach(group => group.classList.remove('active'));
-                            const selectedType = this.value;
-                            const targetGroup = document.querySelector(`.field-group[data-type="${selectedType}"]`);
-                            if (targetGroup) {
-                                targetGroup.classList.add('active');
-                            }
-                        });
-                    });
-
-                    const checkedRadio = document.querySelector('input[name="user_type"]:checked');
-                    if (checkedRadio) {
-                        const targetGroup = document.querySelector(`.field-group[data-type="${checkedRadio.value}"]`);
-                        if (targetGroup) {
-                            targetGroup.classList.add('active');
-                        }
-                    }
-                }
-
-                // Initialize shop selection
-                function initShopSelection() {
-                    let form = document.getElementById('registerform');
-                    form.addEventListener('change', (e) => {
-                        if(e.target.id === 'artist_shop' || e.target.id === 'artist_city'){
-                            let next = e.target.parentNode.nextElementSibling;
-                            let input = next.querySelector('input');
-
-                            if(e.target.value === 'other'){
-                                next.style.display = 'block';
-                                next.style.animation = 'fadeIn 0.3s ease';
-                                input.required = true;
-                                input.focus();
-                            }else{
-                                input.required = false;
-                                input.value = '';
-                            }
-                        }
-                    });
-                }
-
-                // Initialize file upload handling
-                function initFileUpload() {
-                    const fileInput = document.getElementById('certification_file');
-                    const filePreview = document.querySelector('.file-preview');
-                    const filePreviewName = document.querySelector('.file-preview-name');
-                    const fileError = document.querySelector('.file-error');
-                    const removeButton = document.querySelector('.file-preview-remove');
-
-                    if (!fileInput || !filePreview || !filePreviewName || !fileError || !removeButton) {
-                        return;
-                    }
-
-                    const maxSize = parseInt(fileInput.dataset.maxSize || 5242880);
-
-                    fileInput.addEventListener('change', function(e) {
-                        const file = e.target.files[0];
-                        fileError.classList.remove('active');
-
-                        if (file) {
-                            const validTypes = ['.jpg','.jpeg','.png','.gif','.pdf'];
-                            const fileExtension = '.' + file.name.split('.').pop().toLowerCase();
-
-                            if (!validTypes.includes(fileExtension)) {
-                                showError('Please upload a valid file type (JPG, PNG, GIF, or PDF)');
-                                fileInput.value = '';
-                                return;
-                            }
-
-                            if (file.size > maxSize) {
-                                showError('File size must be less than 5MB');
-                                fileInput.value = '';
-                                return;
-                            }
-
-                            filePreviewName.textContent = file.name;
-                            filePreview.classList.add('active');
-                        } else {
-                            filePreview.classList.remove('active');
-                        }
-                    });
-
-                    removeButton.addEventListener('click', function() {
-                        fileInput.value = '';
-                        filePreview.classList.remove('active');
-                        fileError.classList.remove('active');
-                    });
-
-                    function showError(message) {
-                        fileError.textContent = message;
-                        fileError.classList.add('active');
-                        filePreview.classList.remove('active');
-                    }
-                }
-
-                // Initialize all components
-                initUserTypeSelection();
-                initShopSelection();
-                initFileUpload();
-            });
-        </script>
-        <?php
-    }
-
-    /**
-     * Add registration fields
-     */
-    public function addRegistrationFields(): void
-    {
-        echo '<input type="hidden" name="user_pass" value="' . wp_generate_password() . '">';
-        ?>
-        <div class="registration-intro">
-            <?php
-            foreach (JVB_LOGIN['join_intro']??[] as $intro) {
-                echo '<p>'.$intro.'</p>';
-            }
-            ?>
-
-            <?php if ($this->fromFavourites()): ?>
-                <div class="favourites-login-message">
-                    <ul class="benefits-list">
-                        <?php
-                        foreach (JVB_LOGIN['from_favourites_benefits']??[] as $benefit) {
-                            echo '<li>'.$benefit.'</li>';
-                        }
-                        ?>
-                    </ul>
-                </div>
-            <?php endif; ?>
-        </div>
-
-		<?php
-		if (array_key_exists('choose', JVB_LOGIN)) {
-			?>
-			<h3><?= JVB_LOGIN['choose']?></h3>
-			<?php
+		if (!empty($redirect)) {
+			$logout_url = add_query_arg('redirect_to', urlencode($redirect), $logout_url);
 		}
-		?>
 
-		<?php
-		if (count(JVB_USER) > 1) {
-			$this->renderUserTypeSelection();
+		// Add nonce for security
+		return wp_nonce_url($logout_url, 'log-out');
+	}
+	public function resetPasswordUrl(string $url, string $redirect):string
+	{
+		error_log('reset Password Url:'.print_r($url, true));
+		error_log('reset password redirect: '.print_r($redirect, true));
+
+		return str_replace('wp_login.php', 'login/', $url);
+
+	}
+	public function getLoginPage():int|false
+	{
+		return (int)get_option(BASE.'login_page');
+	}
+
+	public function isLoginPage():bool
+	{
+		return is_page($this->getLoginPage());
+	}
+
+	public static function isLogin():bool
+	{
+		$self = new self;
+		return $self->isLoginPage();
+	}
+
+	protected function initMagicLinkSupport(): void
+	{
+		if (!Site::has('magicLink')) {
+			return;
+		}
+	}
+
+	/*********************************************************************
+		RENDERING
+	*********************************************************************/
+	public function renderLoginPage(string $template):string
+	{
+		if (!$this->isLoginPage()) {
+			return $template;
+		}
+		global $_GET;
+		if (is_user_logged_in() && (!array_key_exists('action', $_GET) || $_GET['action']!=='logout')) {
+			wp_redirect(get_home_url(null, '/dash'));
+			exit;
+		}
+		$this->setup();
+		$page = $this->cache->remember(
+				$this->getAction(),
+				function() {
+					return $this->renderPage();
+				},
+				5
+			);
+
+		echo $page;
+		return '';
+	}
+	protected function renderPage() {
+		ob_start();
+		jvbInlineStyles('nav');
+		jvbInlineStyles('dash');
+		jvbInlineStyles('forms');
+		$this->customStyles();
+
+		$this->renderHeader();
+		$this->renderForms();
+		$this->renderFooter();
+
+		return ob_get_clean();
+	}
+
+	protected function getAction():string
+	{
+		if (array_key_exists('action', $_GET)) {
+			switch ($_GET['action']){
+				case 'lostpassword':
+				case 'retrievepassword': // Alias
+					$action = 'lostpassword';
+					break;
+				case 'rp':
+				case 'resetpass':
+					$action = 'resetpass';
+					break;
+				default:
+					$action = $_GET['action'];
+			}
 		} else {
-			?>
-			<p>
-				<label for="first_name" class="required-field">First Name</label>
-				<input type="text" id="first_name" name="first_name" class="input">
-			</p>
-			<p>
-				<label for="email" class="required-field">Email</label>
-				<input type="email" id="email" name="email" class="input">
-			</p>
-			<?php
+			$action = 'login';
 		}
-        if ($this->invitation_data) {
-            ?>
-            <script>
-                document.addEventListener('DOMContentLoaded', function() {
-                    const artistRadio = document.getElementById('artist');
-                    if (artistRadio) {
-                        artistRadio.checked = true;
-                        artistRadio.dispatchEvent(new Event('change'));
-                    }
+		return $action;
+	}
 
-                    const emailField = document.getElementById('artist_email');
-                    if (emailField) {
-                        emailField.value = '<?= esc_js($this->invitation_data['email']); ?>';
-                        emailField.readOnly = true;
-                    }
+	protected function setup():void
+	{
+		$this->action = $this->getAction();
+		if ($this->action == 'logout' || array_key_exists('loggedout', $_GET)) {
+			wp_logout();
+			wp_redirect(esc_attr($_GET['redirect_to'] ?? get_home_url()));
+			exit;
+		}
+		$this->setupLabels();
+		$this->setupFields();
+		$this->setupTitle();
+	}
 
-                    const shopSelect = document.getElementById('artist_shop');
-                    if (shopSelect) {
-                        shopSelect.value = '<?= esc_js($this->invitation_data['shop_id']); ?>';
-                        shopSelect.readOnly = true;
-                    }
-                });
-            </script>
-            <input type="hidden" name="invitation_token" value="<?= sanitize_text_field($_GET['invite']) ?>">
-            <input type="hidden" name="invitation_email" value="<?= sanitize_email($_GET['email']) ?>">
+	protected function setupTitle():void
+	{
+		switch ($this->action) {
+			case 'lostpassword':
+				$title = 'Lost Your Password?';
+				break;
+			case 'resetpass':
+				$title = 'Reset Your Password';
+				break;
+			case 'register':
+				$title = 'Create Your Account';
+				break;
+			default:
+				$title = 'Log In To Your Account';
+		}
+		$this->title = $title;
+	}
+
+	protected function customStyles():void
+	{
+		$logo = get_theme_mod('custom_logo');
+		if ($logo) {
+			$small = wp_get_attachment_image_src($logo, 'medium')[0]??'';
+			$large = wp_get_attachment_image_src($logo, 'large')[0]??'';
+		}
+		echo '<style>
+			.login header,
+			.login footer {
+				display: none;
+			}
+			.login main {
+				display: flex;
+				flex-direction: column;
+				gap: 2rem;
+				justify-content: center;
+				position: relative;
+			}
+			.login .fstatus.fstatus {
+				--wrap: nowrap;
+				top:0;
+				bottom:unset;
+				right: 0;
+			}
+			.login main::before {
+				background-size: 20vw;
+				inset: 0;
+				z-index: 0;
+				content: "";
+				background-image: url("'.$small.'");
+				background-repeat: no-repeat;
+				position: absolute;
+				background-position: 40vw 1rem;
+			}
+			.login main .login-box {
+				--gap: .75rem;
+				padding: 1rem;
+				background-color:rgba(var(--base-rgb),var(--op-6));
+				border-radius: var(--outerRadius);
+				box-shadow: var(--shadow-right), var(--shadow-down);
+				margin: 15vh auto 0!important;
+			}
+				.login main .login-box,
+				.login main .navigation {
+					z-index: 5;
+					max-width: 90vw!important;
+				}
+				.login main .navigation {
+					padding: 0 1rem;
+					margin: 0 auto!important;
+					font-size: var(--small);
+				}
+			.login-box .button {
+				--height: 2.5rem;
+				width: 100%;
+			}
+			.login-box .options {
+				padding: 0 .5rem;
+			}
+			label[for="user_select-subscriber"] {
+			position: absolute;
+			left: var(--offScreen);
+			}
+
+			@media (min-width:768px) {
+				.login main .navigation,
+				.login main .login-box {
+					max-width: 60vw!important;
+					padding-right: 4rem!important;
+					margin: 0 0 0 auto!important;
+				}
+				.login main .login-box {
+					padding: 2rem;
+					--gap: 2rem;
+				}
+				.login main .navigation {
+					padding: 0 var(--offHeight);
+				}
+
+				.login-box .options {
+					padding: 0 4rem;
+				}
+				.login main::before {
+					background-size: 80vw;
+					inset: -5vw;
+					background-image: url("'.$large.'");
+					opacity: .25;
+					transform: rotate(-5deg);
+					background-position: -10vw center;
+				}
+			}
+			</style>';
+	}
+
+	protected function renderForms():void
+	{
+		$form = $this->action.'form';
+		?>
+		<section class="login-box col y-btw">
+			<h1><?=$this->labels['title']?></h1>
+			<?= $this->labels['description'] ?>
+
+			<?= $this->renderLoginForm($this->action); ?>
+
+
+			<?php
+			if (is_array($this->labels['extra'])) {
+				echo '<div class="extra">';
+				foreach($this->labels['extra'] as $extra) {
+					echo '<p>'.$extra.'</p>';
+				}
+				echo '</div>';
+			} else if ($this->labels['extra']!=='') {
+				echo '<div class="extra">'.$this->labels['extra'].'</div>';
+			}
+			?>
+
+			<div class="options row x-btw">
+				<?php
+				switch ($this->action) {
+					case 'login': ?>
+						<a href="<?= add_query_arg('action', 'lostpassword', get_the_permalink()) ?>">Forgot Password?</a>
+						<a href="<?= add_query_arg('action', 'register', get_the_permalink()) ?>">Create Account</a>
+						<?php
+						break;
+					case 'register': ?>
+						<a href="<?= get_the_permalink() ?>">Or Login</a>
+						<a href="<?= add_query_arg('action', 'lostpassword', get_the_permalink()) ?>">Forgot Password?</a>
+						<?php
+						break;
+					case 'lostpassword':
+					case 'magic': ?>
+						<a href="<?= get_the_permalink() ?>">Login Instead</a>
+						<a href="<?= add_query_arg('action', 'register', get_the_permalink()) ?>">Create Account</a>
+						<?php
+						break;
+
+				}
+				 ?>
+
+			</div>
+		</section>
+		<div class="navigation row x-btw">
+			<a href="<?= get_home_url() ?>">Home</a>
+			<?php
+			$privacy = get_privacy_policy_url();
+ 			if ($privacy !== '') { ?>
+				 <a href="<?= $privacy ?>">Our Privacy Policy</a>
+ 			<?php } ?>
+		</div>
+		<?php
+	}
+	public function renderLoginForm(string $action = 'login', string $redirect = '', string $title = ''):string
+	{
+		ob_start();
+		do_action('jvb_add_token_inputs', $this->action);
+		$additionalInputs = ob_get_clean();
+
+		$fields = '';
+		$theFields = $this->getFieldsForAction($action);
+		foreach ($theFields as $name => $config) {
+			$fields .= Form::render($name, '', $config);
+		}
+
+		ob_start();
+		$this->maybeTurnstile();
+		$turnstile = ob_get_clean();
+
+		ob_start();
+		$this->maybeMagicLink();
+		$magicLink = ob_get_clean();
+
+		$redirect = !empty($redirect) ? $redirect : esc_attr($_GET['redirect_to'] ?? '');
+
+		return sprintf(
+			'<form name="%sform" method="post" data-action="jvb_%s">
+				%s%s%s
+				<input type="hidden" name="action" value="jvb_%s">
+				<input type="hidden" name="redirect_to" value="%s">
+				<input type="hidden" name="request_id" value="%s">
+				%s
+				%s
+				%s
+				%s
+				 <div class="row x-btw nowrap">
+					<button type="submit" class="button button-primary button-large">%s</button>
+					%s
+				</div>
+			</form>',
+			$action,
+			$action,
+			jvbFormStatus(),
+			$title,
+			wp_nonce_field('jvb_'.$action),
+			$action,
+			$redirect,
+			wp_generate_password(16, false),
+			($action === 'magic') ? '<input type="hidden" name="type" value="login">' : '',
+			$additionalInputs,
+			$fields,
+			$turnstile,
+			$this->labels['submit'],
+			$magicLink
+		);
+	}
+	protected function renderHeader():void
+    {
+    ?>
+        <!DOCTYPE html>
+    <html <?php language_attributes(); ?>>
+        <head>
+            <title><?= $this->title ?> | <?= get_bloginfo('name') ?></title>
+            <meta charset="<?php bloginfo('charset'); ?>">
+            <meta name="viewport" content="width=device-width, initial-scale=1.0">
+			 <link rel="preconnect" href="<?= get_home_url()?>"/>
+            <?php wp_head(); ?>
+        </head>
+    <body class="login">
+        <?php jvbAccessibility();?>
+        <header>
             <?php
-        }
+            $checked = (is_user_logged_in() && current_user_can('prefers_dark_theme', true)) ? ' checked' : '';
+            $title = ($checked == '') ? 'Toggle Dark Mode' : 'Toggle Light Mode';
+            echo '<label title="'.$title.'" id="theme-switch" class="switch" for="theme-switcher">
+    				<span class="screen-reader-text">Toggle dark mode</span>
+                    <input class="theme-switch row" id="theme-switcher" name="theme-switcher" type="checkbox"'.$checked.' data-setting="theme" data-theme name="dark-mode" aria-label="Toggle dark mode"><span class="slider">'.
+					jvbIcon('sun-dim', ['title'=> 'Light Mode']).
+					jvbIcon('moon', ['title'=>'Dark Mode']).
+					'</span></label>';
+            ?>
+            <p class="title">
+                <a href="<?= get_home_url(); ?>" rel="home" title="Back to Site">
+                    <?php
+                    $icon = (int) get_option( 'site_icon' );
+					$out = '';
+					if ($icon > 0) {
+						$url = wp_get_attachment_image_url( $icon);
+						if ($url) {
+							$out = '<img src="'.$url.'">';
+						}
+					}
+					if ($out == '') {
+						$out =jvbIcon('house');
+					}
+                    ?><?= $out ?>
+                </a>
+            </p>
+        </header>
+        <main>
+    <?php
     }
 
-	protected function renderUserTypeSelection():void
+	protected function renderFooter():void
+    {
+        ?>
+
+        <footer class="col">
+        	<?= $this->labels['footer'] ?>
+        	<?= jvbLoadingScreen() ?>
+        	<?= TaxonomySelector::outputSelectorModal() ?>
+        	<?php
+			do_action('jvbLoginFooter');
+ 			?>
+ 			<p>Made with ♡ by <a href="https://jakevan.ca/">JakeVan</a></p>
+        </footer>
+
+        <?php wp_footer(); ?>
+
+        </body>
+        </html>
+
+        <?php
+    }
+
+	/**********************************************************************
+		TOKEN PROCESSING
+	**********************************************************************/
+	protected function processTokenHandlers(int $user_id, string $email): void
 	{
+		foreach ($this->tokenHandlers as $priority => $handlers) {
+			foreach ($handlers as $token_key => $handler) {
+				if (isset($_POST[$token_key]) || isset($_GET[$token_key])) {
+					$token_value = $_POST[$token_key] ?? $_GET[$token_key];
+					call_user_func($handler, sanitize_text_field($token_value), $email, $user_id);
+				}
+			}
+		}
+	}
 
+	/*************************************************************************
+ 	*	SECURITY & VALIDATION
+	*************************************************************************/
 
-        // Get list of tattoo shops and cities
-        $shops = get_terms(array(
-            'taxonomy' => 'jvb_shop',
-            'hide_empty' => true
-        ));
+	protected function checkRequestId(): bool
+	{
+		$request_id = $_POST['request_id'] ?? '';
+		if (empty($request_id)) {
+			return true; // No request_id provided, allow (for backward compat)
+		}
 
-        $cities = get_terms(array(
-            'taxonomy' => 'jvb_city',
-            'hide_empty' => false,
-        ));
+		$cache_key = 'request_' . $request_id;
+		if (get_transient($cache_key)) {
+			return false; // Duplicate request
+		}
+
+		// Store request ID for 1 minute to prevent duplicates
+		set_transient($cache_key, true, 60);
+		return true;
+	}
+
+	protected function maybeTurnstile(): void
+	{
+		if (!Site::hasIntegration('cloudflare')) {
+			return;
+		}
+		JVB()->connect('cloudflare')->renderTurnstile();
+	}
+
+	protected function maybeTurnstileScripts(): void
+	{
+		if (!Site::hasIntegration('cloudflare')) {
+			return;
+		}
+		JVB()->connect('cloudflare')->enqueueTurnstileScripts();
+	}
+
+	protected function verifyTurnstile(): bool
+	{
+		if (!Site::hasIntegration('cloudflare')) {
+			return true; // Not enabled, pass verification
+		}
+
+		$token = $_POST['cf-turnstile-response'] ?? '';
+		if (empty($token)) {
+			return false;
+		}
+
+		return JVB()->connect('cloudflare')->verifyTurnstile($token);
+	}
+
+	/************************************************************************
+		LABELS & UI
+	************************************************************************/
+	protected function setupLabels(): void
+	{
+		$default = $this->getDefaultLabels();
+		$default = apply_filters('jvbLoginLabels', $default, $_GET);
+
+		if(array_key_exists('type', $_GET) && $_GET['type'] === 'favourites') {
+			if (array_key_exists('favourites', JVB_LOGIN)) {
+				foreach (JVB_LOGIN['favourites'] as $key => $value) {
+					$default[$key] = $value;
+				}
+			}
+		}
+
+		foreach (['description', 'footer', 'extra'] as $location) {
+			if ($default[$location] === '') {
+				continue;
+			}
+			if (empty($default[$location])) {
+				$default[$location] = '';
+				continue;
+			}
+			$text = (!is_array($default[$location])) ? [$default[$location]] : $default[$location];
+
+			if (!empty($text)) {
+				$default[$location] = '<div class="'.$location.'">';
+				foreach ($text as $d) {
+					$default[$location] .= '<p>'.$d.'</p>';
+				}
+				$default[$location] .= '</div>';
+			}
+		}
+		$this->labels = $default;
+	}
+
+	protected function getDefaultLabels(): array
+	{
+		switch ($this->action) {
+			case 'register':
+				return Site::login()->getLabels('register');
+			case 'lostpassword':
+				return Site::login()->getLabels('lostPassword');
+			case 'resetpass':
+			case 'rp':
+				return Site::login()->getLabels('resetPassword');
+			case 'logout':
+				return Site::login()->getLabels('logout');
+			case 'magic':
+				return Site::login()->getLabels('magic');
+			case 'login':
+			default:
+				return Site::login()->getLabels('login');
+		}
+	}
+
+	protected function maybeMagicLink(): void
+	{
+		if (!JVB()->magicLink() || !in_array($this->action, ['login', 'lostpassword'])) {
+			return;
+		}
 		?>
-		<div class="user-type-section">
-
-            <?php
-            $i = 1;
-            $radio = '<input type="radio" id="user0" name="user_type" value="subscriber" required checked>
-            <label for="user0"></label>';
-            $descriptions = '';
-            foreach (JVB_USER as $role => $config) {
-                if (jvbCheck('can_register', $config)) {
-                    $radio .= '<input type="radio" id="user'.$i.'" name="user_type" value="'.$role.'" required';
-                    $radio .= ($role === 'enthusiast' && $this->fromFavourites()) ? 'checked' : '';
-                    $radio .= '><label for="user'.$i.'">'.jvbIcon($role, ['title' =>$config['label'], 'size'=>40]).'<h4>'.$config['label'].'</h4><p>';
-                    $radio .=  $config['join_text']??'';
-                    $radio .= '</p></label>';
-
-                    $descriptions .= '<div class="user'.$i.'">'.is_array($config['join_description']) ? implode('', array_map(function ($item) { return '<p>'.$item.'</p>'; }, $config['join_description'])) : '<p>'.$config['join_description'].'</p>'.'</div>';
-
-                    $i++;
-                }
-            }
-
-            echo $radio;
-            echo $descriptions;
-            ?>
-            <input type="radio" id="enthusiast" name="user_type" value="enthusiast" required <?= ($this->fromFavourites()) ? 'checked' : '' ?>>
-            <label for="enthusiast"><?=jvbIcon('heart', ['title' =>'Enthusiast', 'size'=>40])?><h4>Enthusiast</h4><p>Start here.</p></label>
-            <input type="radio" id="artist" name="user_type" value="artist" required>
-            <label for="artist"><?=jvbIcon('tattoo', ['title'=> 'Artist', 'size'=> 40])?><h4>Artist</h4><p>Show your talent.</p></label>
-            <input type="radio" id="partner" name="user_type" value="partner" required>
-            <label for="partner"><?=jvbIcon('partner', ['title'=>'Partner', 'size' => 40])?><h4>Partner</h4><p>Support the community.</p></label>
-            <p class="enthusiast">Save your favourites. Get notified.</p>
-            <p class="artist">Show off your work.</p>
-            <p class="partner">Support the community.</p>
-        </div>
-
-        <!-- Enthusiast Fields -->
-        <div class="field-group" data-type="enthusiast">
-            <h4>Welcome to the scene.</h4>
-            <p>Sign up with your email to:</p>
-            <ul>
-                <li>Save your favourites for easy access</li>
-                <li>Get notified when your favourite artists add new content</li>
-                <li>Stay in the loop with local flash days and events</li>
-                <li>Discover styles and artists that match your vision</li>
-            </ul>
-            <p>
-                <label for="enthusiast_first_name" class="required-field">First Name</label>
-                <input type="text" id="enthusiast_first_name" name="enthusiast_first_name" class="input">
-            </p>
-            <p>
-                <label for="enthusiast_email" class="required-field">Email</label>
-                <input type="email" id="enthusiast_email" name="enthusiast_email" class="input">
-            </p>
-            <div><p><b>BONUS</b>: Everything's free. And always will be. We work with partners chosen by and for the community to keep the lights on.</p></div>
-        </div>
-
-        <!-- Artist Fields -->
-        <div class="field-group" data-type="artist">
-            <h4>Welcome to the scene!</h4>
-            <p>We'll start small, with the basics. Before your profile goes live, we need to verify:</p>
-            <ul>
-                <li>you are who you say you are</li>
-                <li>you work at the shop you listed</li>
-                <li>your certification</li>
-            </ul>
-            <p>
-                <label for="artist_first_name" class="required-field">First Name</label>
-                <input type="text" id="artist_first_name" name="artist_first_name" class="input">
-            </p>
-            <p>
-                <label for="artist_last_name" class="required-field">Last Name</label>
-                <input type="text" id="artist_last_name" name="artist_last_name" class="input">
-            </p>
-            <p>
-                <label for="artist_email" class="required-field">Email</label>
-                <input type="email" id="artist_email" name="artist_email" class="input">
-            </p>
-            <p>
-                <label for="artist_shop" class="required-field">Shop</label>
-                <select id="artist_shop" name="artist_shop" class="input">
-                    <option value="">Select a shop</option>
-                    <option value="other">Add New Shop</option>
-                    <?php foreach ($shops as $shop) : ?>
-                        <option value="<?= esc_attr($shop->term_id); ?>"><?= esc_html($shop->name); ?></option>
-                    <?php endforeach; ?>
-                </select>
-            </p>
-            <p id="other_shop_field" style="display: none;">
-                <label for="artist_shop_other" class="required-field">Shop Name</label>
-                <input type="text" id="artist_shop_other" name="artist_shop_other" class="input" placeholder="Shop name">
-            </p>
-
-            <p>
-                <label for="artist_type" class="required-field">Type</label>
-                <input type="radio" id="type-tattoo-artist" name="artist_type" value="tattoo-artist">
-                <label for="type-tattoo-artist">Tattoo Artist</label>
-                <input type="radio" id="type-piercer" name="artist_type" value="piercer">
-                <label for="type-piercer">Piercer</label>
-                <input type="radio" id="type-other" name="artist_type" value="other">
-                <label for="type-other">Other</label>
-            </p>
-            <p>
-                <label for="artist_city" class="required-field">City</label>
-                <select id="artist_city" name="artist_city" class="input">
-                    <option value="">Select a city</option>
-                    <option value="other">Add New City</option>
-                    <?php foreach ($cities as $city) : ?>
-                        <option value="<?= esc_attr($city->term_id); ?>"><?= esc_html($city->name); ?></option>
-                    <?php endforeach; ?>
-                </select>
-            </p>
-            <p id="other_city_field" style="display: none;">
-                <label for="artist_city_other" class="required-field">City Name</label>
-                <input type="text" id="artist_city_other" name="artist_city_other" class="input" placeholder="City">
-            </p>
-
-            <div class="file-upload-container">
-                <label class="file-upload-label">Certification or Training Documents</label>
-                <p><i>Optional</i> — If you've been certified in bloodborne pathogen safety, or any other tattoo safety course, pass along your certificate. This just eases the verification process.</p>
-                <div class="file-upload-wrapper">
-                    <input type="file" name="certification_file" id="certification_file" accept=".jpg,.jpeg,.png,.gif,.pdf" data-max-size="<?= $this->max_file_size; ?>">
-                    <p class="file-upload-text">
-                        <strong>Click to upload</strong> or drag and drop<br>
-                        JPG, PNG, GIF or PDF (max. 5MB)
-                    </p>
-                </div>
-                <div class="file-preview">
-                    <div class="file-preview-content">
-                        <span class="file-preview-name"></span>
-                        <button type="button" class="file-preview-remove">Remove</button>
-                    </div>
-                </div>
-                <div class="file-error"></div>
-            </div>
-            <p>Once you click register:</p>
-            <ul>
-                <li>We'll start looking into your information (usually within 24-48 hours)</li>
-                <li>You'll get a password reset email</li>
-                <li>Upon setting your password, you can start filling in your profile - but it won't go live until we've verified your information.</li>
-            </ul>
-            <p>If you have any questions or concerns - or anything you'd like to follow up on - email us at get@edmonton.ink or message us on <a target="_blank" href="https://www.instagram.com/edmonton.ink/" title="@edmonton.ink on Instagram">Instagram</a>.</p>
-            <div><p><b>BONUS</b>: Everything's free. And always will be. We work with partners chosen by and for the community to keep the lights on.</p></div>
-        </div>
-
-        <!-- Partner Fields -->
-        <div class="field-group" data-type="partner">
-            <h4>Howdy, partner!</h4>
-            <p>We appreciate your interest!</p>
-            <p>edmonton.ink is a great place to showcase what you do, whether you:</p>
-            <ul>
-                <li>provide goods or services that tattoo artists could use</li>
-                <li>provide goods or services that are tattoo adjacent (such as art, merch, etc)</li>
-                <li>provide goods or services that folks who love tattoos could also love</li>
-            </ul>
-
-            <p>We'll start with some basics, then we'll reach out to follow up (usually within 24-48 hours).</p>
-            <p>
-                <label for="partner_name" class="required-field">Contact Name</label>
-                <input type="text" id="partner_name" name="partner_name" class="input">
-            </p>
-            <p>
-                <label for="partner_email" class="required-field">Email</label>
-                <input type="email" id="partner_email" name="partner_email" class="input">
-            </p>
-            <p>
-                <label for="partner_business" class="required-field">Business Name</label>
-                <input type="text" id="partner_business" name="partner_business" class="input">
-            </p>
-            <p>
-                <label for="partner_website">Business Website</label>
-                <input type="url" id="partner_website" name="partner_website" class="input">
-            </p>
-            <p>
-                <label for="partner_description">Why would you be a good fit?</label>
-                <textarea id="partner_description" name="partner_description" rows="8"></textarea>
-            </p>
-            <p><i>Note:</i> — you must have good standing in the tattoo community to stay a partner of edmonton.ink.</p>
-            <p>If we receive multiple requests to terminate a partnership with you from member artists, we reserve the right to cancel your listings.</p>
-        </div>
+		<a class="button" href="<?= add_query_arg('action', 'magic', wp_login_url()) ?>" title="Email yourself a link to log you in auto-magically!">
+			<?= jvbIcon('magic-wand'); ?>
+			Magic Link
+		</a>
 		<?php
 	}
 
-    /**
-     * Registration errors filter
-     */
-    public function registrationErrorsFilter(WP_Error $errors, string $sanitized_user_login, string $user_email): WP_Error
-    {
-        error_log('Registration Data: '.print_r($_POST, true));
-        $user_type = isset($_POST['user_type']) ? $_POST['user_type'] : '';
 
-        if (empty($user_type)) {
-            $errors->add('user_type_error', 'Please select your user type.');
-            return $errors;
-        }
-
-        // Get email based on user type
-        $email_field = $user_type . '_email';
-        $email = isset($_POST[$email_field]) ? sanitize_email($_POST[$email_field]) : '';
-
-        // Remove WordPress's default username error
-        $errors = new WP_Error();
-
-        // If this is an invited artist, validate the invitation
-        $invite = (array_key_exists('invite_token', $_POST)) ? sanitize_text_field($_POST['invite_token']) : false;
-        if ($invite && array_key_exists('role', $_POST)) {
-            $handler = JVB()->routes('invites');
-            $invitation = $handler->verifyInvitation($invite, sanitize_email($_POST['invite_email']), sanitize_text_field($_POST['role']));
-
-            if (!$invitation) {
-                $errors->add('invalid_invitation', 'Invalid invitation token.');
-            } elseif (strtotime($invitation->expires_at) < current_time('timestamp')) {
-                $errors->add('expired_invitation', 'This invitation has expired.');
-            }
-        }
-
-        // Validate email first
-        if (empty($email)) {
-            $errors->add('email_error', 'Email is required.');
-        } elseif (!is_email($email)) {
-            $errors->add('email_error', 'Please enter a valid email address.');
-        } elseif (email_exists($email)) {
-            $errors->add('email_error', 'This email is already registered.');
-        }
-
-        switch ($user_type) {
-            case 'enthusiast':
-                if (empty($_POST['enthusiast_first_name'])) {
-                    $errors->add('first_name_error', 'First name is required.');
-                }
-                break;
-
-            case 'artist':
-                $required_fields = array(
-                    'artist_first_name' => 'First name',
-                    'artist_last_name' => 'Last name',
-                    'artist_shop' => 'Shop',
-                    'artist_city' => 'City',
-                    'artist_type' => 'Type',
-                );
-                foreach ($required_fields as $field => $label) {
-                    if (empty($_POST[$field])) {
-                        $errors->add($field . '_error', $label . ' is required.');
-                    }
-                }
-                break;
-
-            case 'partner':
-                $required_fields = array(
-                    'partner_name' => 'Contact name',
-                    'partner_business' => 'Business name'
-                );
-
-                foreach ($required_fields as $field => $label) {
-                    if (empty($_POST[$field])) {
-                        $errors->add($field . '_error', $label . ' is required.');
-                    }
-                }
-                break;
-        }
-
-        if (isset($_POST['user_type']) && $_POST['user_type'] === 'artist' && !empty($_FILES['certification_file']['name'])) {
-            $file = $_FILES['certification_file'];
-
-            // Validate file type
-            if (!in_array($file['type'], $this->allowed_file_types)) {
-                $errors->add('file_type_error', 'Please upload a valid file type (JPG, PNG, GIF, or PDF)');
-            }
-
-            // Validate file size
-            if ($file['size'] > $this->max_file_size) {
-                $errors->add('file_size_error', 'File size must be less than 5MB');
-            }
-        }
-
-        return $errors;
+	/************************************************************************
+		SCRIPTS
+	************************************************************************/
+	public function enqueueScripts(): void
+{
+    if (!$this->isLoginPage()) {
+        return;
     }
 
-    /**
-     * Save registration fields
-     */
-    public function saveRegistrationFields(int $user_id, array $userdata): void
-    {
-        $user_type = isset($_POST['user_type']) ? $_POST['user_type'] : false;
-        if (!$user_type) {
-            return;
-        }
+    $this->maybeTurnstileScripts();
+    wp_enqueue_script('jvb-form');
+    $action = $this->getAction();
 
-        // Set user role based on type
-        $user = new WP_User($user_id);
-        $caps = JVB()->roles();
-        $email = false;
-        $upload_dir = wp_upload_dir();
-        $base_dir = $upload_dir['basedir'];
+    $redirect_to = isset($_GET['redirect_to']) ? esc_url_raw($_GET['redirect_to']) : '';
+    $has_turnstile = Site::hasIntegration('cloudflare');
 
-        switch ($user_type) {
-            case 'artist':
-                $user->set_role('jvb_artist');
-                $user->remove_role('subscriber');
+    ob_start();
 
-                $email = sanitize_email($_POST['artist_email']);
-                $first = sanitize_text_field($_POST['artist_first_name']);
-                $last = sanitize_text_field($_POST['artist_last_name']);
-                $display_name = $first . ' ' . $last;
+    ?>
 
-                // Save artist fields
-                $temp = wp_update_user([
-                    'ID' => $user_id,
-                    'first_name' => $first,
-                    'last_name' => $last,
-                    'display_name' => $display_name
-                ]);
-                $user = get_userdata($temp);
+	document.addEventListener('DOMContentLoaded', async function () {
+		const hasTurnstile = <?= json_encode($has_turnstile) ?>;
+		const redirectTo = <?= json_encode($redirect_to) ?>;
 
-                $link = $caps->addUserLink($user, 'artist');
-                $meta = new MetaManager($link, 'post');
-				$meta->setAll([
-					'first_name'	=> $first,
-					'email'			=> $email
-				]);
+		window.auth.subscribe(event => {
+			if (event === 'auth-loaded') {
+				const form = document.querySelector('.login form');
+				if (!form || !window.jvbForm) return;
 
-                // If this was an invited artist, handle the invitation
-                if (array_key_exists('invite_token', $_POST)) {
-                    $handler = JVB()->routes('invites');
-                    $handler->acceptInvitation(sanitize_text_field($_POST['invite_token']), sanitize_email($_POST['invite_email']), $user->ID);
-                }
+				window.jvbForm.registerForm(form, {
+					endpoint: '<?= $action ?>',
+					showStatus: false,
+					cache: false,
+				});
 
-                if (absint($_POST['artist_shop']) > 0) {
-                    JVB()->routes('shop')->requestShopAdmission($user_id, absint($_POST['artist_shop']));
-                }
-                if (absint($_POST['artist_city']) > 0) {
-                    wp_set_post_terms($link, (int)absint($_POST['artist_city']), BASE.'city');
-                }
+				window.jvbForm.subscribe((event, data) => {
+					if (event === 'form-submit') {
+						const { config } = data;
+						const formElement = config.element;
 
-                //Create approval request and notify verified users
-                JVB()->routes('approvals')->createArtistApprovalRequest($user_id);
+						// Collect current form data
+						const formData = new FormData(formElement);
+						const formObject = Object.fromEntries(formData.entries());
 
-                //Make base directories
-                $artist_dir = $base_dir . '/artists/' . $user_id;
-                wp_mkdir_p($artist_dir);
-                wp_mkdir_p($artist_dir . '/artwork');
-                wp_mkdir_p($artist_dir . '/events');
-                wp_mkdir_p($artist_dir . '/profile');
-                wp_mkdir_p($artist_dir . '/temp');
+						let params = new URLSearchParams(window.location.search);
+						if (params.has('key')) {
+							formObject['key'] = params.get('key');
+						}
+						if (params.has('login')) {
+							formObject['login'] = params.get('login');
+						}
 
-                switch ($_POST['artist_type']) {
-                    case 'tattoo-artist':
-                        $caps->setUserAs($user, 'tattoo-artist');
-                        $term = get_term_by('name', 'Tattoo Artists', BASE.'type');
-                        if ($term && !is_wp_error($term)) {
-                            wp_set_post_terms($link, $term->term_id, BASE.'type');
-                        }
-                        wp_mkdir_p($artist_dir . '/tattoos');
-                        break;
-                    case 'piercer':
-                        $caps->setUserAs($user, 'piercer');
-                        $term = get_term_by('name', 'Piercers', BASE.'type');
-                        if ($term && !is_wp_error($term)) {
-                            wp_set_post_terms($link, $term->term_id, BASE.'type');
-                        }
-                        wp_mkdir_p($artist_dir . '/piercings');
-                        break;
-                }
-                break;
+						// Add redirect_to from URL
+						if (redirectTo) {
+							formObject.redirect_to = redirectTo;
+						}
 
-            case 'partner':
-                $user->set_role('jvb_partner');
-                $user->remove_role('subscriber');
-                $name = sanitize_text_field($_POST['partner_name']);
-                $email = sanitize_email($_POST['partner_email']);
+						const submit = formElement.querySelector('[type=submit]');
+						const oldText = submit.textContent;
 
-                $caps->setUserAs($user, 'partner');
-                $link = $caps->addUserLink($user, 'partner');
+						window.jvbForm.showFormStatus(config.id, 'uploading');
 
-                // Save partner fields
-                update_user_meta($user_id, 'contact_name', sanitize_text_field($_POST['partner_name']));
-                update_user_meta($user_id, 'business_name', sanitize_text_field($_POST['partner_business']));
-                update_user_meta($user_id, 'business_website', esc_url_raw($_POST['partner_website']));
+						submit.disabled = true;
+						submit.textContent = 'Loading...';
 
-                // Create partner base directory
-                $partner_dir = $base_dir . '/partners/' . $user_id;
-                wp_mkdir_p($partner_dir);
-                wp_mkdir_p($partner_dir . '/offers');
-                wp_mkdir_p($partner_dir . '/events');
-                wp_mkdir_p($partner_dir . '/profile');
-                wp_mkdir_p($partner_dir . '/temp');
-                break;
+						window.auth.fetch(`${jvbSettings.api}auth/<?= $action ?>`, {
+							method: 'POST',
+							body: JSON.stringify(formObject)
+						})
+						.then(response => response.json().then(result => ({ response, result })))
+						.then(({ response, result }) => {
+							if (!response.ok) {
+								window.jvbForm.showFormStatus(config.id, 'error');
+								window.jvbForm.handleFormError(formElement, result);
+								return;
+							}
 
-            case 'enthusiast':
-                $user->set_role('jvb_enthusiast');
-                $user->remove_role('subscriber');
-                $caps->setUserAs($user, 'enthusiast');
-                $name = sanitize_text_field($_POST['enthusiast_first_name']);
-                $email = sanitize_email($_POST['enthusiast_email']);
+							window.jvbForm.showFormStatus(config.id, 'submitted');
 
-                // Save enthusiast fields
-                $temp = wp_update_user([
-                    'ID' => $user_id,
-                    'first_name' => $name,
-                    'user_email' => $email,
-                ]);
-                break;
-        }
+							if (result.message) {
+								window.jvbForm.handleFormSuccess(formElement, result);
+							}
 
-        // Handle file upload for artists
-        if (isset($_POST['user_type']) && $_POST['user_type'] === 'artist' && !empty($_FILES['certification_file']['name'])) {
-            $file = $_FILES['certification_file'];
+							if (window.auth?.handleLogin && result.auth) {
+								return window.auth.handleLogin(result.auth).then(() => {
+									if (result.redirect) {
+										setTimeout(() => {
+											window.location.href = result.redirect;
+										}, 20);
+									}
+								});
+							} else if (result.redirect) {
+								setTimeout(() => {
+									window.location.href = result.redirect;
+								}, 20);
+							}
+						})
+						.catch(error => {
+							console.error('Form submission error:', error);
+							window.jvbForm.showFormStatus(config.id, 'error');
+							window.jvbForm.handleFormError(formElement, {
+								message: 'Network error. Please check your connection and try again.',
+								code: 'network_error'
+							});
+						})
+						.finally(() => {
+							submit.textContent = oldText;
+							submit.disabled = false;
+						});
+					}
+				});
+			}
+		});
+	});
 
-            // Setup upload directory
-            $upload_dir = wp_upload_dir();
-            $user_directory = 'artist-certifications/' . $user_id;
-            $target_dir = $upload_dir['basedir'] . '/' . $user_directory;
+	<?php
+		$script = ob_get_clean();
+		wp_add_inline_script('jvb-form', $script);
+	}
 
-            // Create directory if it doesn't exist
-            wp_mkdir_p($target_dir);
+	/*************************************************************************
+		SUCCESS HANDLING
+	*************************************************************************/
+	public function handleSuccessfulLogin(string $username, WP_User $user): void
+	{
+		if (isOurPeople() && !user_can($user, 'manage_options')) {
+			wp_redirect(get_home_url(null, '/dash'));
+			exit;
+		}
+	}
 
-            // Generate unique filename
-            $file_extension = pathinfo($file['name'], PATHINFO_EXTENSION);
-            $filename = 'certification-' . time() . '.' . $file_extension;
-            $target_file = $target_dir . '/' . $filename;
 
-            // Move uploaded file
-            if (move_uploaded_file($file['tmp_name'], $target_file)) {
-                // Save file information in user meta
-                update_user_meta($user_id, 'certification_file', array(
-                    'url' => $upload_dir['baseurl'] . '/' . $user_directory . '/' . $filename,
-                    'file' => $target_file,
-                    'type' => $file['type'],
-                    'original_name' => $file['name']
-                ));
-            }
-        }
+	/**
+	 * Handle login errors
+	 */
+	protected function handleLoginError(WP_Error $error): void
+	{
+		$login_url = wp_login_url();
+		$login_url = add_query_arg('login_error', urlencode($error->get_error_code()), $login_url);
 
-        // Handle list invitation acceptance
-        if (isset($_GET['list_token']) && !empty($_GET['list_token']) && isset($_GET['email'])) {
-            $token = sanitize_text_field($_GET['list_token']);
-            $email = sanitize_email($_GET['email']);
+		if (isset($_REQUEST['redirect_to'])) {
+			$login_url = add_query_arg('redirect_to', urlencode($_REQUEST['redirect_to']), $login_url);
+		}
 
-            if ($email) {
-                JVB()->routes('favourites')->acceptListInvitation($token, $email, $user_id);
-            }
-        }
-    }
+		wp_safe_redirect($login_url);
+		exit;
+	}
 
-    /**
-     * Registration success message
-     */
-    public function registrationSuccessMessage(WP_Error $errors, string $redirect_to): WP_Error
-    {
-        if (isset($errors->errors['registered']) && isset($_POST['invitation_token'])) {
-            // Custom message for invited artists
-            $message = "WELCOME ABOARD!<br><br>" .
-                "Password setup is in your inbox. <br>" .
-                "Since you were invited by a shop, you can skip the verification wait and start building your profile right away! ♡";
+	public function saveRegistrationFields(int $user_id, array $userdata):void
+	{
 
-            unset($errors->errors['registered']);
-            $errors->add('registered', $message, 'message');
-        }
-
-        if (isset($errors->errors['registered'])) {
-            $user_type = isset($_POST['user_type']) ? $_POST['user_type'] : 'user';
-
-            switch ($user_type) {
-                case 'enthusiast':
-                    $message = "YOU'RE IN!<br><br>Check your inbox - we've sent password setup details.<br>Get ready to build your dream artist collection! ♡";
-                    break;
-
-                case 'artist':
-                    $message = "HELL YEAH!<br><br>Password setup is in your inbox. <br>While we verify your info (24-48hrs), you can start building your profile. <br>Just remember - it stays underground until you're cleared. ♡";
-                    break;
-
-                case 'partner':
-                    $message = "ROCK ON!<br><br>Check your inbox - we've sent password setup details.<br>We'll check out your pitch in the next 24-48hrs. <br><br>Meanwhile, you can start prepping your presence - but you won't hit the streets until we give the nod. ♡";
-                    break;
-
-                default:
-                    $message = "YOU'RE ON THE LIST!<br><br>Check your inbox for the next steps. ♡";
-            }
-
-            // Replace the default message
-            unset($errors->errors['registered']);
-            $errors->add('registered', $message, 'message');
-        }
-
-        return $errors;
-    }
-
-    /**
-     * Check if registration is from invite
-     */
-    protected function fromInvite(): bool
-    {
-        return isset($_GET['invite']) && isset($_GET['email']);
-    }
-
-    /**
-     * Custom register message
-     */
-    public function customRegisterMessage(string $message): string
-    {
-        return "Join Edmonton's tattoo community";
-    }
+	}
+	public function setAction(string $action = 'login'):void
+	{
+		$this->action = $action;
+		$this->setup();
+	}
 }
 
-// Initialize the consolidated auth manager
+// Initialize the login manager
 new LoginManager();

--
Gitblit v1.10.0