+
\ No newline at end of file
diff --git a/app/templates/vendor/partials/sidebar.html b/app/templates/vendor/partials/sidebar.html
index 566549bd..0ed6e900 100644
--- a/app/templates/vendor/partials/sidebar.html
+++ b/app/templates/vendor/partials/sidebar.html
@@ -1,10 +1,187 @@
-
-
-
-
- Title
-
-
+{# app/templates/vendor/partials/sidebar.html #}
+{#
+Vendor sidebar - loads vendor data client-side via JavaScript
+Follows same pattern as admin sidebar
+#}
-
-
\ No newline at end of file
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/templates/vendor/partials/vendor_info.html b/app/templates/vendor/partials/vendor_info.html
index 566549bd..5996dfa2 100644
--- a/app/templates/vendor/partials/vendor_info.html
+++ b/app/templates/vendor/partials/vendor_info.html
@@ -1,10 +1,58 @@
-
-
-
-
- Title
-
-
+{# app/templates/vendor/partials/vendor_info.html #}
+{#
+This component loads vendor data client-side via JavaScript
+No server-side vendor data required - follows same pattern as admin pages
+#}
+
+
+
+
+ ๐ช
+
+
+
+
+
+
-
-
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+
+
+ โข .platform.com
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/main.py b/main.py
index 0b96bb52..f1ca6c4d 100644
--- a/main.py
+++ b/main.py
@@ -1,11 +1,19 @@
# main.py
+import sys
+import io
+
+# Fix Windows console encoding issues (must be at the very top)
+if sys.platform == 'win32':
+ sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
+ sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
+
import logging
from datetime import datetime, timezone
from pathlib import Path
-from fastapi import Depends, FastAPI, HTTPException, Request
+from fastapi import Depends, FastAPI, HTTPException, Request, Response
from fastapi.middleware.cors import CORSMiddleware
-from fastapi.responses import HTMLResponse, RedirectResponse
+from fastapi.responses import HTMLResponse, RedirectResponse, FileResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from sqlalchemy import text
@@ -73,10 +81,49 @@ else:
# Include API router (JSON endpoints at /api/*)
app.include_router(api_router, prefix="/api")
+
+# ============================================================================
+# FAVICON ROUTES (Must be registered BEFORE page routers)
# ============================================================================
-# OLD: Keep frontend router for now (we'll phase it out)
-# app.include_router(frontend_router)
+def serve_favicon() -> Response:
+ """
+ Serve favicon with caching headers.
+ Checks multiple possible locations for the favicon.
+ """
+ # Possible favicon locations (in priority order)
+ possible_paths = [
+ STATIC_DIR / "favicon.ico",
+ STATIC_DIR / "images" / "favicon.ico",
+ STATIC_DIR / "assets" / "favicon.ico",
+ ]
+
+ # Find first existing favicon
+ for favicon_path in possible_paths:
+ if favicon_path.exists():
+ return FileResponse(
+ favicon_path,
+ media_type="image/x-icon",
+ headers={
+ "Cache-Control": "public, max-age=86400", # Cache for 1 day
+ }
+ )
+
+ # No favicon found - return 204 No Content
+ return Response(status_code=204)
+
+
+@app.get("/favicon.ico", include_in_schema=False)
+async def favicon():
+ """Serve favicon from root path."""
+ return serve_favicon()
+
+
+@app.get("/vendor/favicon.ico", include_in_schema=False)
+async def vendor_favicon():
+ """Handle vendor-prefixed favicon requests."""
+ return serve_favicon()
+
# ============================================================================
# HTML PAGE ROUTES (Jinja2 Templates)
@@ -93,6 +140,7 @@ app.include_router(
# Vendor pages
app.include_router(
vendor_pages.router,
+ prefix="/vendor",
tags=["vendor-pages"],
include_in_schema=False # Don't show HTML pages in API docs
)
@@ -100,10 +148,12 @@ app.include_router(
# Shop pages
app.include_router(
shop_pages.router,
+ prefix="/shop",
tags=["shop-pages"],
include_in_schema=False # Don't show HTML pages in API docs
)
+
# ============================================================================
# API ROUTES (JSON Responses)
# ============================================================================
@@ -163,4 +213,5 @@ async def documentation():
if __name__ == "__main__":
import uvicorn
- uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
+
+ uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
\ No newline at end of file
diff --git a/middleware/vendor_context.py b/middleware/vendor_context.py
index 0e0dfa43..9b78564f 100644
--- a/middleware/vendor_context.py
+++ b/middleware/vendor_context.py
@@ -30,7 +30,7 @@ class VendorContextManager:
host = request.headers.get("host", "")
path = request.url.path
- # Remove port from host if present (e.g., localhost:8000 โ localhost)
+ # Remove port from host if present (e.g., localhost:8000 -> localhost)
if ":" in host:
host = host.split(":")[0]
@@ -118,7 +118,7 @@ class VendorContextManager:
logger.warning(f"Vendor for domain {domain} is not active")
return None
- logger.info(f"โ Vendor found via custom domain: {domain} โ {vendor.name}")
+ logger.info(f"[OK] Vendor found via custom domain: {domain} -> {vendor.name}")
return vendor
else:
logger.warning(f"No active vendor found for custom domain: {domain}")
@@ -137,7 +137,7 @@ class VendorContextManager:
if vendor:
method = context.get("detection_method", "unknown")
- logger.info(f"โ Vendor found via {method}: {subdomain} โ {vendor.name}")
+ logger.info(f"[OK] Vendor found via {method}: {subdomain} -> {vendor.name}")
else:
logger.warning(f"No active vendor found for subdomain: {subdomain}")
@@ -184,19 +184,49 @@ class VendorContextManager:
"""Check if request is for API endpoints."""
return request.url.path.startswith("/api/")
+ @staticmethod
+ def is_static_file_request(request: Request) -> bool:
+ """Check if request is for static files."""
+ path = request.url.path.lower()
+
+ # Static file extensions
+ static_extensions = (
+ '.ico', '.css', '.js', '.png', '.jpg', '.jpeg', '.gif', '.svg',
+ '.woff', '.woff2', '.ttf', '.eot', '.webp', '.map', '.json',
+ '.xml', '.txt', '.pdf', '.webmanifest'
+ )
+
+ # Static paths
+ static_paths = ('/static/', '/media/', '/assets/', '/.well-known/')
+
+ # Check if it's a static file by extension
+ if path.endswith(static_extensions):
+ return True
+
+ # Check if it's in a static directory
+ if any(path.startswith(static_path) for static_path in static_paths):
+ return True
+
+ # Special case: favicon.ico at any level
+ if 'favicon.ico' in path:
+ return True
+
+ return False
+
async def vendor_context_middleware(request: Request, call_next):
"""
Middleware to inject vendor context into request state.
Handles three routing modes:
- 1. Custom domains (customdomain1.com โ Vendor 1)
- 2. Subdomains (vendor1.platform.com โ Vendor 1)
- 3. Path-based (/vendor/vendor1/ โ Vendor 1)
+ 1. Custom domains (customdomain1.com -> Vendor 1)
+ 2. Subdomains (vendor1.platform.com -> Vendor 1)
+ 3. Path-based (/vendor/vendor1/ -> Vendor 1)
"""
- # Skip vendor detection for admin, API, and system requests
+ # Skip vendor detection for admin, API, static files, and system requests
if (VendorContextManager.is_admin_request(request) or
VendorContextManager.is_api_request(request) or
+ VendorContextManager.is_static_file_request(request) or
request.url.path in ["/", "/health", "/docs", "/redoc", "/openapi.json"]):
return await call_next(request)
@@ -217,12 +247,12 @@ async def vendor_context_middleware(request: Request, call_next):
)
logger.debug(
- f"๐ช Vendor context: {vendor.name} ({vendor.subdomain}) "
+ f"[VENDOR] Vendor context: {vendor.name} ({vendor.subdomain}) "
f"via {vendor_context['detection_method']}"
)
else:
logger.warning(
- f"โ ๏ธ Vendor not found for context: {vendor_context}"
+ f"[WARNING] Vendor not found for context: {vendor_context}"
)
request.state.vendor = None
request.state.vendor_context = vendor_context
diff --git a/scripts/route_diagnostics.py b/scripts/route_diagnostics.py
new file mode 100644
index 00000000..0616e221
--- /dev/null
+++ b/scripts/route_diagnostics.py
@@ -0,0 +1,160 @@
+#!/usr/bin/env python3
+"""
+FastAPI Route Diagnostics Script
+
+Run this script to check if your vendor routes are properly configured.
+Usage: python route_diagnostics.py
+"""
+
+import sys
+from typing import List, Dict
+
+
+def check_route_order():
+ """Check if routes are registered in the correct order."""
+ print("๐ Checking FastAPI Route Configuration...\n")
+
+ try:
+ # Try to import the vendor router
+ from app.api.v1.vendor import router as vendor_router
+
+ print("โ Successfully imported vendor router\n")
+
+ # Check if routes are registered
+ routes = vendor_router.routes
+ print(f"๐ Total routes registered: {len(routes)}\n")
+
+ # Analyze route order
+ json_routes = []
+ html_routes = []
+
+ for route in routes:
+ if hasattr(route, 'path'):
+ path = route.path
+ methods = getattr(route, 'methods', set())
+
+ # Determine if JSON or HTML based on common patterns
+ if 'login' in path or 'dashboard' in path or 'products' in path:
+ # Check response class if available
+ if hasattr(route, 'response_class'):
+ response_class = str(route.response_class)
+ if 'HTML' in response_class:
+ html_routes.append((path, methods))
+ else:
+ json_routes.append((path, methods))
+ else:
+ # Check endpoint name/tags
+ endpoint = getattr(route, 'endpoint', None)
+ if endpoint and 'page' in endpoint.__name__.lower():
+ html_routes.append((path, methods))
+ else:
+ json_routes.append((path, methods))
+
+ print("๐ Route Analysis:")
+ print(f" JSON API routes: {len(json_routes)}")
+ print(f" HTML page routes: {len(html_routes)}\n")
+
+ # Check for specific vendor info route
+ vendor_info_found = False
+ for route in routes:
+ if hasattr(route, 'path'):
+ if '/{vendor_code}' == route.path and 'GET' in getattr(route, 'methods', set()):
+ vendor_info_found = True
+ print("โ Found vendor info endpoint: GET /{vendor_code}")
+ break
+
+ if not vendor_info_found:
+ print("โ WARNING: Vendor info endpoint (GET /{vendor_code}) not found!")
+ print(" This is likely causing your JSON parsing error.")
+ print(" Action required: Add the vendor info endpoint\n")
+ return False
+
+ print("\nโ Route configuration looks good!")
+ return True
+
+ except ImportError as e:
+ print(f"โ Error importing vendor router: {e}")
+ print(" Make sure you're running this from your project root")
+ return False
+ except Exception as e:
+ print(f"โ Unexpected error: {e}")
+ return False
+
+
+def test_vendor_endpoint():
+ """Test if the vendor endpoint returns JSON."""
+ print("\n๐งช Testing Vendor Endpoint...\n")
+
+ try:
+ import requests
+
+ # Test with a sample vendor code
+ vendor_code = "WIZAMART"
+ url = f"http://localhost:8000/api/v1/vendor/{vendor_code}"
+
+ print(f"๐ก Making request to: {url}")
+
+ response = requests.get(url, timeout=5)
+
+ print(f" Status Code: {response.status_code}")
+ print(f" Content-Type: {response.headers.get('content-type', 'N/A')}")
+
+ # Check if response is JSON
+ content_type = response.headers.get('content-type', '')
+ if 'application/json' in content_type:
+ print("โ Response is JSON")
+ data = response.json()
+ print(f" Vendor: {data.get('name', 'N/A')}")
+ print(f" Code: {data.get('vendor_code', 'N/A')}")
+ return True
+ elif 'text/html' in content_type:
+ print("โ ERROR: Response is HTML, not JSON!")
+ print(" This confirms the route ordering issue")
+ print(" The HTML page route is catching the API request")
+ return False
+ else:
+ print(f"โ ๏ธ Unknown content type: {content_type}")
+ return False
+
+ except requests.exceptions.ConnectionError:
+ print("โ ๏ธ Cannot connect to server. Is FastAPI running on localhost:8000?")
+ return None
+ except Exception as e:
+ print(f"โ Error testing endpoint: {e}")
+ return False
+
+
+def main():
+ """Run all diagnostics."""
+ print("=" * 60)
+ print("FastAPI Vendor Route Diagnostics")
+ print("=" * 60 + "\n")
+
+ # Check route configuration
+ config_ok = check_route_order()
+
+ # Test actual endpoint
+ endpoint_ok = test_vendor_endpoint()
+
+ # Summary
+ print("\n" + "=" * 60)
+ print("SUMMARY")
+ print("=" * 60)
+
+ if config_ok and endpoint_ok:
+ print("โ All checks passed! Your vendor routes are configured correctly.")
+ elif config_ok is False:
+ print("โ Route configuration issues detected.")
+ print(" Action: Add vendor info endpoint and check route order")
+ elif endpoint_ok is False:
+ print("โ Endpoint is returning HTML instead of JSON.")
+ print(" Action: Check route registration order in vendor/__init__.py")
+ elif endpoint_ok is None:
+ print("โ ๏ธ Could not test endpoint (server not running)")
+ print(" Action: Start your FastAPI server and run this script again")
+
+ print("\n๐ See TROUBLESHOOTING_GUIDE.md for detailed solutions")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/static/shared/img/about.txt b/static/shared/img/about.txt
new file mode 100644
index 00000000..b1508b1a
--- /dev/null
+++ b/static/shared/img/about.txt
@@ -0,0 +1,6 @@
+This favicon was generated using the following font:
+
+- Font Title: Kotta One
+- Font Author: undefined
+- Font Source: https://fonts.gstatic.com/s/kottaone/v20/S6u_w41LXzPc_jlfNWqPHA3s5dwt7w.ttf
+- Font License: undefined)
diff --git a/static/shared/img/android-chrome-192x192.png b/static/shared/img/android-chrome-192x192.png
new file mode 100644
index 00000000..7dd15e09
Binary files /dev/null and b/static/shared/img/android-chrome-192x192.png differ
diff --git a/static/shared/img/android-chrome-512x512.png b/static/shared/img/android-chrome-512x512.png
new file mode 100644
index 00000000..b3f3979c
Binary files /dev/null and b/static/shared/img/android-chrome-512x512.png differ
diff --git a/static/shared/img/apple-touch-icon.png b/static/shared/img/apple-touch-icon.png
new file mode 100644
index 00000000..0bf8b3a5
Binary files /dev/null and b/static/shared/img/apple-touch-icon.png differ
diff --git a/static/shared/img/favicon-16x16.png b/static/shared/img/favicon-16x16.png
new file mode 100644
index 00000000..6c87727e
Binary files /dev/null and b/static/shared/img/favicon-16x16.png differ
diff --git a/static/shared/img/favicon-32x32.png b/static/shared/img/favicon-32x32.png
new file mode 100644
index 00000000..dc956b8a
Binary files /dev/null and b/static/shared/img/favicon-32x32.png differ
diff --git a/static/shared/img/favicon.ico b/static/shared/img/favicon.ico
new file mode 100644
index 00000000..d762762d
Binary files /dev/null and b/static/shared/img/favicon.ico differ
diff --git a/static/shared/img/site.webmanifest b/static/shared/img/site.webmanifest
new file mode 100644
index 00000000..45dc8a20
--- /dev/null
+++ b/static/shared/img/site.webmanifest
@@ -0,0 +1 @@
+{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
\ No newline at end of file
diff --git a/static/vendor/js/dashboard.js b/static/vendor/js/dashboard.js
new file mode 100644
index 00000000..4be60267
--- /dev/null
+++ b/static/vendor/js/dashboard.js
@@ -0,0 +1,81 @@
+// app/static/vendor/js/dashboard.js
+/**
+ * Vendor dashboard page logic
+ */
+
+function vendorDashboard() {
+ return {
+ loading: false,
+ error: '',
+ stats: {
+ products_count: 0,
+ orders_count: 0,
+ customers_count: 0,
+ revenue: 0
+ },
+ recentOrders: [],
+ recentProducts: [],
+
+ async init() {
+ await this.loadDashboardData();
+ },
+
+ async loadDashboardData() {
+ this.loading = true;
+ this.error = '';
+
+ try {
+ // Load stats
+ const statsResponse = await apiClient.get(
+ `/api/v1/vendors/${this.vendorCode}/stats`
+ );
+ this.stats = statsResponse;
+
+ // Load recent orders
+ const ordersResponse = await apiClient.get(
+ `/api/v1/vendors/${this.vendorCode}/orders?limit=5&sort=created_at:desc`
+ );
+ this.recentOrders = ordersResponse.items || [];
+
+ // Load recent products
+ const productsResponse = await apiClient.get(
+ `/api/v1/vendors/${this.vendorCode}/products?limit=5&sort=created_at:desc`
+ );
+ this.recentProducts = productsResponse.items || [];
+
+ logInfo('Dashboard data loaded', {
+ stats: this.stats,
+ orders: this.recentOrders.length,
+ products: this.recentProducts.length
+ });
+
+ } catch (error) {
+ logError('Failed to load dashboard data', error);
+ this.error = 'Failed to load dashboard data. Please try refreshing the page.';
+ } finally {
+ this.loading = false;
+ }
+ },
+
+ async refresh() {
+ await this.loadDashboardData();
+ },
+
+ formatCurrency(amount) {
+ return new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'EUR'
+ }).format(amount || 0);
+ },
+
+ formatDate(dateString) {
+ if (!dateString) return '';
+ const date = new Date(dateString);
+ return date.toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric'
+ });
+ }
+ };
+}
\ No newline at end of file
diff --git a/static/vendor/js/init-alpine.js b/static/vendor/js/init-alpine.js
new file mode 100644
index 00000000..b194df85
--- /dev/null
+++ b/static/vendor/js/init-alpine.js
@@ -0,0 +1,104 @@
+// app/static/vendor/js/init-alpine.js
+/**
+ * Alpine.js initialization for vendor pages
+ * Provides common data and methods for all vendor pages
+ */
+
+function data() {
+ return {
+ dark: false,
+ isSideMenuOpen: false,
+ isNotificationsMenuOpen: false,
+ isProfileMenuOpen: false,
+ currentPage: '',
+ currentUser: {},
+ vendor: null,
+ vendorCode: null,
+
+ init() {
+ // Set current page from URL
+ const path = window.location.pathname;
+ const segments = path.split('/').filter(Boolean);
+ this.currentPage = segments[segments.length - 1] || 'dashboard';
+
+ // Get vendor code from URL
+ if (segments[0] === 'vendor' && segments[1]) {
+ this.vendorCode = segments[1];
+ }
+
+ // Load user from localStorage
+ const user = localStorage.getItem('currentUser');
+ if (user) {
+ this.currentUser = JSON.parse(user);
+ }
+
+ // Load theme preference
+ const theme = localStorage.getItem('theme');
+ if (theme === 'dark') {
+ this.dark = true;
+ }
+
+ // Load vendor info
+ this.loadVendorInfo();
+ },
+
+ async loadVendorInfo() {
+ if (!this.vendorCode) return;
+
+ try {
+ const response = await apiClient.get(`/api/v1/vendors/${this.vendorCode}`);
+ this.vendor = response;
+ logDebug('Vendor info loaded', this.vendor);
+ } catch (error) {
+ logError('Failed to load vendor info', error);
+ }
+ },
+
+ toggleSideMenu() {
+ this.isSideMenuOpen = !this.isSideMenuOpen;
+ },
+
+ closeSideMenu() {
+ this.isSideMenuOpen = false;
+ },
+
+ toggleNotificationsMenu() {
+ this.isNotificationsMenuOpen = !this.isNotificationsMenuOpen;
+ if (this.isNotificationsMenuOpen) {
+ this.isProfileMenuOpen = false;
+ }
+ },
+
+ closeNotificationsMenu() {
+ this.isNotificationsMenuOpen = false;
+ },
+
+ toggleProfileMenu() {
+ this.isProfileMenuOpen = !this.isProfileMenuOpen;
+ if (this.isProfileMenuOpen) {
+ this.isNotificationsMenuOpen = false;
+ }
+ },
+
+ closeProfileMenu() {
+ this.isProfileMenuOpen = false;
+ },
+
+ toggleTheme() {
+ this.dark = !this.dark;
+ localStorage.setItem('theme', this.dark ? 'dark' : 'light');
+ },
+
+ async handleLogout() {
+ try {
+ await apiClient.post('/api/v1/vendor/auth/logout');
+ } catch (error) {
+ logError('Logout error', error);
+ } finally {
+ localStorage.removeItem('accessToken');
+ localStorage.removeItem('currentUser');
+ window.location.href = `/vendor/${this.vendorCode}/login`;
+ }
+ }
+ };
+}
\ No newline at end of file
diff --git a/static/vendor/js/login.js b/static/vendor/js/login.js
new file mode 100644
index 00000000..4d74f213
--- /dev/null
+++ b/static/vendor/js/login.js
@@ -0,0 +1,110 @@
+// app/static/vendor/js/login.js
+/**
+ * Vendor login page logic
+ */
+
+// โ Use centralized logger - ONE LINE!
+// Create custom logger for login page
+const loginLog = window.LogConfig.createLogger('LOGIN');
+
+function vendorLogin() {
+ return {
+ credentials: {
+ username: '',
+ password: ''
+ },
+ vendor: null,
+ vendorCode: null,
+ loading: false,
+ checked: false,
+ error: '',
+ success: '',
+ errors: {},
+ dark: false,
+
+ async init() {
+ // Load theme
+ const theme = localStorage.getItem('theme');
+ if (theme === 'dark') {
+ this.dark = true;
+ }
+
+ // Get vendor code from URL path
+ const pathSegments = window.location.pathname.split('/').filter(Boolean);
+ if (pathSegments[0] === 'vendor' && pathSegments[1]) {
+ this.vendorCode = pathSegments[1];
+ await this.loadVendor();
+ }
+ this.checked = true;
+ },
+
+ async loadVendor() {
+ this.loading = true;
+ try {
+ const response = await apiClient.get(`/vendor/${this.vendorCode}`);
+ this.vendor = response;
+ logInfo('Vendor loaded', this.vendor);
+ } catch (error) {
+ logError('Failed to load vendor', error);
+ this.error = 'Failed to load vendor information';
+ } finally {
+ this.loading = false;
+ }
+ },
+
+ async handleLogin() {
+ this.clearErrors();
+ this.loading = true;
+
+ try {
+ if (!this.credentials.username) {
+ this.errors.username = 'Username is required';
+ }
+ if (!this.credentials.password) {
+ this.errors.password = 'Password is required';
+ }
+
+ if (Object.keys(this.errors).length > 0) {
+ this.loading = false;
+ return;
+ }
+
+ const response = await apiClient.post('/vendor/auth/login', {
+ username: this.credentials.username,
+ password: this.credentials.password,
+ vendor_code: this.vendorCode
+ });
+
+ logInfo('Login successful', response);
+
+ localStorage.setItem('accessToken', response.access_token);
+ localStorage.setItem('currentUser', JSON.stringify(response.user));
+ localStorage.setItem('vendorCode', this.vendorCode);
+
+ this.success = 'Login successful! Redirecting...';
+
+ setTimeout(() => {
+ window.location.href = `/vendor/${this.vendorCode}/dashboard`;
+ }, 1000);
+
+ } catch (error) {
+ logError('Login failed', error);
+
+ if (error.status === 401) {
+ this.error = 'Invalid username or password';
+ } else if (error.status === 403) {
+ this.error = 'Your account does not have access to this vendor';
+ } else {
+ this.error = error.message || 'Login failed. Please try again.';
+ }
+ } finally {
+ this.loading = false;
+ }
+ },
+
+ clearErrors() {
+ this.error = '';
+ this.errors = {};
+ }
+ };
+}
\ No newline at end of file