Compare commits

...

6 Commits

Author SHA1 Message Date
ca04de4885 Add favicon icons and update product/about pages 2026-02-18 23:02:58 -06:00
82428c5589 Web Update 2026-02-18 23:00:58 -06:00
d5a0f1169c chore: Add logs directory to gitignore to exclude runtime log files 2026-02-04 00:42:51 -06:00
9a7b00649b feat: Implement comprehensive OAuth and email verification authentication system
- Add email verification with token-based validation
- Integrate Google, Facebook, and Yahoo OAuth providers
- Add OAuth configuration and email service modules
- Update User model with email_verified, oauth_provider, oauth_id fields
- Implement async password hashing/verification to prevent blocking
- Add database migration script for new user fields
- Create email verification page with professional UI
- Update login page with social login buttons (Google, Facebook, Yahoo)
- Add OAuth callback token handling
- Implement scroll-to-top navigation component
- Add 5-second real-time polling for Products and Services pages
- Enhance About page with Apple-style scroll animations
- Update Home and Contact pages with branding and business info
- Optimize API cache with prefix-based clearing
- Create comprehensive setup documentation and quick start guide
- Fix login performance with ThreadPoolExecutor for bcrypt operations

Performance improvements:
- Login time optimized to ~220ms with async password verification
- Real-time data updates every 5 seconds
- Non-blocking password operations

Security enhancements:
- Email verification required for new accounts
- OAuth integration for secure social login
- Verification tokens expire after 24 hours
- Password field nullable for OAuth users
2026-02-04 00:41:16 -06:00
72f17c8be9 Fix HTML rendering for service descriptions, allow zero price for services, improve image_url handling 2026-02-01 22:31:00 -06:00
d3cad0e5fa Add aiosqlite for async SQLite support 2026-01-28 20:44:21 -06:00
62 changed files with 9578 additions and 509 deletions

258
)') Normal file
View File

@@ -0,0 +1,258 @@
SSUUMMMMAARRYY OOFF LLEESSSS CCOOMMMMAANNDDSS
Commands marked with * may be preceded by a number, _N.
Notes in parentheses indicate the behavior if _N is given.
A key preceded by a caret indicates the Ctrl key; thus ^K is ctrl-K.
h H Display this help.
q :q Q :Q ZZ Exit.
---------------------------------------------------------------------------
MMOOVVIINNGG
e ^E j ^N CR * Forward one line (or _N lines).
y ^Y k ^K ^P * Backward one line (or _N lines).
f ^F ^V SPACE * Forward one window (or _N lines).
b ^B ESC-v * Backward one window (or _N lines).
z * Forward one window (and set window to _N).
w * Backward one window (and set window to _N).
ESC-SPACE * Forward one window, but don't stop at end-of-file.
d ^D * Forward one half-window (and set half-window to _N).
u ^U * Backward one half-window (and set half-window to _N).
ESC-) RightArrow * Right one half screen width (or _N positions).
ESC-( LeftArrow * Left one half screen width (or _N positions).
ESC-} ^RightArrow Right to last column displayed.
ESC-{ ^LeftArrow Left to first column.
F Forward forever; like "tail -f".
ESC-F Like F but stop when search pattern is found.
r ^R ^L Repaint screen.
R Repaint screen, discarding buffered input.
---------------------------------------------------
Default "window" is the screen height.
Default "half-window" is half of the screen height.
---------------------------------------------------------------------------
SSEEAARRCCHHIINNGG
/_p_a_t_t_e_r_n * Search forward for (_N-th) matching line.
?_p_a_t_t_e_r_n * Search backward for (_N-th) matching line.
n * Repeat previous search (for _N-th occurrence).
N * Repeat previous search in reverse direction.
ESC-n * Repeat previous search, spanning files.
ESC-N * Repeat previous search, reverse dir. & spanning files.
ESC-u Undo (toggle) search highlighting.
ESC-U Clear search highlighting.
&_p_a_t_t_e_r_n * Display only matching lines.
---------------------------------------------------
A search pattern may begin with one or more of:
^N or ! Search for NON-matching lines.
^E or * Search multiple files (pass thru END OF FILE).
^F or @ Start search at FIRST file (for /) or last file (for ?).
^K Highlight matches, but don't move (KEEP position).
^R Don't use REGULAR EXPRESSIONS.
^W WRAP search if no match found.
---------------------------------------------------------------------------
JJUUMMPPIINNGG
g < ESC-< * Go to first line in file (or line _N).
G > ESC-> * Go to last line in file (or line _N).
p % * Go to beginning of file (or _N percent into file).
t * Go to the (_N-th) next tag.
T * Go to the (_N-th) previous tag.
{ ( [ * Find close bracket } ) ].
} ) ] * Find open bracket { ( [.
ESC-^F _<_c_1_> _<_c_2_> * Find close bracket _<_c_2_>.
ESC-^B _<_c_1_> _<_c_2_> * Find open bracket _<_c_1_>.
---------------------------------------------------
Each "find close bracket" command goes forward to the close bracket
matching the (_N-th) open bracket in the top line.
Each "find open bracket" command goes backward to the open bracket
matching the (_N-th) close bracket in the bottom line.
m_<_l_e_t_t_e_r_> Mark the current top line with <letter>.
M_<_l_e_t_t_e_r_> Mark the current bottom line with <letter>.
'_<_l_e_t_t_e_r_> Go to a previously marked position.
'' Go to the previous position.
^X^X Same as '.
ESC-M_<_l_e_t_t_e_r_> Clear a mark.
---------------------------------------------------
A mark is any upper-case or lower-case letter.
Certain marks are predefined:
^ means beginning of the file
$ means end of the file
---------------------------------------------------------------------------
CCHHAANNGGIINNGG FFIILLEESS
:e [_f_i_l_e] Examine a new file.
^X^V Same as :e.
:n * Examine the (_N-th) next file from the command line.
:p * Examine the (_N-th) previous file from the command line.
:x * Examine the first (or _N-th) file from the command line.
:d Delete the current file from the command line list.
= ^G :f Print current file name.
---------------------------------------------------------------------------
MMIISSCCEELLLLAANNEEOOUUSS CCOOMMMMAANNDDSS
-_<_f_l_a_g_> Toggle a command line option [see OPTIONS below].
--_<_n_a_m_e_> Toggle a command line option, by name.
__<_f_l_a_g_> Display the setting of a command line option.
___<_n_a_m_e_> Display the setting of an option, by name.
+_c_m_d Execute the less cmd each time a new file is examined.
!_c_o_m_m_a_n_d Execute the shell command with $SHELL.
|XX_c_o_m_m_a_n_d Pipe file between current pos & mark XX to shell command.
s _f_i_l_e Save input to a file.
v Edit the current file with $VISUAL or $EDITOR.
V Print version number of "less".
---------------------------------------------------------------------------
OOPPTTIIOONNSS
Most options may be changed either on the command line,
or from within less by using the - or -- command.
Options may be given in one of two forms: either a single
character preceded by a -, or a name preceded by --.
-? ........ --help
Display help (from command line).
-a ........ --search-skip-screen
Search skips current screen.
-A ........ --SEARCH-SKIP-SCREEN
Search starts just after target line.
-b [_N] .... --buffers=[_N]
Number of buffers.
-B ........ --auto-buffers
Don't automatically allocate buffers for pipes.
-c ........ --clear-screen
Repaint by clearing rather than scrolling.
-d ........ --dumb
Dumb terminal.
-D xx_c_o_l_o_r . --color=xx_c_o_l_o_r
Set screen colors.
-e -E .... --quit-at-eof --QUIT-AT-EOF
Quit at end of file.
-f ........ --force
Force open non-regular files.
-F ........ --quit-if-one-screen
Quit if entire file fits on first screen.
-g ........ --hilite-search
Highlight only last match for searches.
-G ........ --HILITE-SEARCH
Don't highlight any matches for searches.
-h [_N] .... --max-back-scroll=[_N]
Backward scroll limit.
-i ........ --ignore-case
Ignore case in searches that do not contain uppercase.
-I ........ --IGNORE-CASE
Ignore case in all searches.
-j [_N] .... --jump-target=[_N]
Screen position of target lines.
-J ........ --status-column
Display a status column at left edge of screen.
-k [_f_i_l_e] . --lesskey-file=[_f_i_l_e]
Use a lesskey file.
-K ........ --quit-on-intr
Exit less in response to ctrl-C.
-L ........ --no-lessopen
Ignore the LESSOPEN environment variable.
-m -M .... --long-prompt --LONG-PROMPT
Set prompt style.
-n -N .... --line-numbers --LINE-NUMBERS
Don't use line numbers.
-o [_f_i_l_e] . --log-file=[_f_i_l_e]
Copy to log file (standard input only).
-O [_f_i_l_e] . --LOG-FILE=[_f_i_l_e]
Copy to log file (unconditionally overwrite).
-p [_p_a_t_t_e_r_n] --pattern=[_p_a_t_t_e_r_n]
Start at pattern (from command line).
-P [_p_r_o_m_p_t] --prompt=[_p_r_o_m_p_t]
Define new prompt.
-q -Q .... --quiet --QUIET --silent --SILENT
Quiet the terminal bell.
-r -R .... --raw-control-chars --RAW-CONTROL-CHARS
Output "raw" control characters.
-s ........ --squeeze-blank-lines
Squeeze multiple blank lines.
-S ........ --chop-long-lines
Chop (truncate) long lines rather than wrapping.
-t [_t_a_g] .. --tag=[_t_a_g]
Find a tag.
-T [_t_a_g_s_f_i_l_e] --tag-file=[_t_a_g_s_f_i_l_e]
Use an alternate tags file.
-u -U .... --underline-special --UNDERLINE-SPECIAL
Change handling of backspaces.
-V ........ --version
Display the version number of "less".
-w ........ --hilite-unread
Highlight first new line after forward-screen.
-W ........ --HILITE-UNREAD
Highlight first new line after any forward movement.
-x [_N[,...]] --tabs=[_N[,...]]
Set tab stops.
-X ........ --no-init
Don't use termcap init/deinit strings.
-y [_N] .... --max-forw-scroll=[_N]
Forward scroll limit.
-z [_N] .... --window=[_N]
Set size of window.
-" [_c[_c]] . --quotes=[_c[_c]]
Set shell quote characters.
-~ ........ --tilde
Don't display tildes after end of file.
-# [_N] .... --shift=[_N]
Set horizontal scroll amount (0 = one half screen width).
--file-size
Automatically determine the size of the input file.
--follow-name
The F command changes files if the input file is renamed.
--incsearch
Search file as each pattern character is typed in.
--line-num-width=N
Set the width of the -N line number field to N characters.
--mouse
Enable mouse input.
--no-keypad
Don't send termcap keypad init/deinit strings.
--no-histdups
Remove duplicates from command history.
--rscroll=C
Set the character used to mark truncated lines.
--save-marks
Retain marks across invocations of less.
--status-col-width=N
Set the width of the -J status column to N characters.
--use-backslash
Subsequent options use backslash as escape char.
--use-color
Enables colored text.
--wheel-lines=N
Each click of the mouse wheel moves N lines.
---------------------------------------------------------------------------
LLIINNEE EEDDIITTIINNGG
These keys can be used to edit text being entered
on the "command line" at the bottom of the screen.
RightArrow ..................... ESC-l ... Move cursor right one character.
LeftArrow ...................... ESC-h ... Move cursor left one character.
ctrl-RightArrow ESC-RightArrow ESC-w ... Move cursor right one word.
ctrl-LeftArrow ESC-LeftArrow ESC-b ... Move cursor left one word.
HOME ........................... ESC-0 ... Move cursor to start of line.
END ............................ ESC-$ ... Move cursor to end of line.
BACKSPACE ................................ Delete char to left of cursor.
DELETE ......................... ESC-x ... Delete char under cursor.
ctrl-BACKSPACE ESC-BACKSPACE ........... Delete word to left of cursor.
ctrl-DELETE .... ESC-DELETE .... ESC-X ... Delete word under cursor.
ctrl-U ......... ESC (MS-DOS only) ....... Delete entire line.
UpArrow ........................ ESC-k ... Retrieve previous command line.
DownArrow ...................... ESC-j ... Retrieve next command line.
TAB ...................................... Complete filename & cycle.
SHIFT-TAB ...................... ESC-TAB Complete filename & reverse cycle.
ctrl-L ................................... Complete filename, list all.

2
.gitignore vendored
View File

@@ -41,6 +41,8 @@ yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
dump.rdb
logs/
*.log
# System files
.DS_Store

View File

@@ -3,4 +3,19 @@ DB_NAME="test_database"
CORS_ORIGINS="*"
JWT_SECRET="techzone-super-secret-key-2024-production"
DATABASE_URL="postgresql+asyncpg://techzone_user:techzone_pass@localhost:5432/techzone"
PORT=8181
PORT=8181
# Notification Settings
# SMTP Email Configuration (Gmail example)
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=
SMTP_PASSWORD=
# Admin notification email
ADMIN_EMAIL=admin@prompttech.com
# WhatsApp Notification (CallMeBot API)
# Get API key from: https://www.callmebot.com/blog/free-api-whatsapp-messages/
ADMIN_PHONE=+5016261234
WHATSAPP_API_URL=https://api.callmebot.com/whatsapp.php
WHATSAPP_API_KEY=

38
backend/.env.example Normal file
View File

@@ -0,0 +1,38 @@
# JWT Configuration
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production-use-long-random-string
# Email Configuration (Gmail SMTP)
# Follow steps in docs/AUTH_SETUP_GUIDE.md to get App Password
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=prompttechbz@gmail.com
SMTP_PASSWORD=your-16-char-app-password-here
FROM_EMAIL=prompttechbz@gmail.com
# Frontend URL
FRONTEND_URL=http://localhost:5300
# Google OAuth
# Get from: https://console.cloud.google.com/
GOOGLE_CLIENT_ID=your-google-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-your-google-client-secret
GOOGLE_REDIRECT_URI=http://localhost:8181/api/auth/google/callback
# Facebook OAuth
# Get from: https://developers.facebook.com/
FACEBOOK_APP_ID=your-facebook-app-id
FACEBOOK_APP_SECRET=your-facebook-app-secret
FACEBOOK_REDIRECT_URI=http://localhost:8181/api/auth/facebook/callback
# Yahoo OAuth
# Get from: https://developer.yahoo.com/
YAHOO_CLIENT_ID=your-yahoo-client-id
YAHOO_CLIENT_SECRET=your-yahoo-client-secret
YAHOO_REDIRECT_URI=http://localhost:8181/api/auth/yahoo/callback
# Admin Configuration
ADMIN_EMAIL=prompttechbz@gmail.com
ADMIN_PHONE=+5016261234
# Database (if needed)
DATABASE_URL=postgresql://user:password@localhost:5432/dbname

64
backend/check_admin.py Normal file
View File

@@ -0,0 +1,64 @@
#!/usr/bin/env python3
"""Check and create admin user"""
import asyncio
import sys
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from sqlalchemy import select
import bcrypt
from datetime import datetime, timezone
sys.path.append('/media/pts/Website/PromptTech_Solution_Site/backend')
from models import User, UserRole
from database import DATABASE_URL
async def check_and_create_admin():
engine = create_async_engine(DATABASE_URL, echo=False)
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async with async_session() as session:
# Check for existing admin
result = await session.execute(select(User))
users = result.scalars().all()
print(f"\n📊 Current users in database:")
for user in users:
print(f" - {user.email} (Role: {user.role.value})")
# Check if admin@prompttech.com exists
result = await session.execute(
select(User).where(User.email == "admin@prompttech.com")
)
admin = result.scalar_one_or_none()
if admin:
print(f"\n✅ Admin user already exists: admin@prompttech.com")
print(f" If you can't login, the password should be: admin123")
else:
print(f"\n⚠️ Admin user not found. Creating new admin...")
# Create admin user
email = "admin@prompttech.com"
password = "admin123"
name = "Admin User"
hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
admin_user = User(
email=email,
name=name,
password=hashed_password,
role=UserRole.ADMIN,
created_at=datetime.now(timezone.utc)
)
session.add(admin_user)
await session.commit()
print(f"\n✅ Admin user created!")
print(f" Email: {email}")
print(f" Password: {password}")
if __name__ == "__main__":
asyncio.run(check_and_create_admin())

View File

@@ -0,0 +1,78 @@
#!/usr/bin/env python3
"""
Create properly sized favicon images from the logo
"""
from PIL import Image
import os
# Paths
logo_path = "/media/pts/Website/PromptTech_Solution_Site/Logo/PTB-logo.png"
public_dir = "/media/pts/Website/PromptTech_Solution_Site/frontend/public"
# Load the original logo
print("Loading logo...")
logo = Image.open(logo_path)
print(f"Original size: {logo.size}")
# Convert to RGBA if needed
if logo.mode != 'RGBA':
logo = logo.convert('RGBA')
# Define sizes to create
sizes = [
(16, 16, "favicon-16x16.png"),
(32, 32, "favicon-32x32.png"),
(48, 48, "favicon-48x48.png"),
(64, 64, "favicon-64x64.png"),
(180, 180, "apple-touch-icon.png"),
(192, 192, "android-chrome-192x192.png"),
(512, 512, "android-chrome-512x512.png"),
]
# Create each size
for width, height, filename in sizes:
# Create a square canvas
size = (width, height)
# Resize with high quality
resized = logo.copy()
resized.thumbnail(size, Image.Resampling.LANCZOS)
# Create a new image with transparent background
new_img = Image.new('RGBA', size, (255, 255, 255, 0))
# Calculate position to center the logo
paste_x = (size[0] - resized.size[0]) // 2
paste_y = (size[1] - resized.size[1]) // 2
# Paste the resized logo
new_img.paste(resized, (paste_x, paste_y), resized)
# Save
output_path = os.path.join(public_dir, filename)
new_img.save(output_path, "PNG", optimize=True)
print(f"Created: {filename} ({width}x{height})")
# Create favicon.ico with multiple sizes
print("\nCreating favicon.ico with multiple sizes...")
ico_sizes = [(16, 16), (32, 32), (48, 48)]
ico_images = []
for width, height in ico_sizes:
resized = logo.copy()
resized.thumbnail((width, height), Image.Resampling.LANCZOS)
# Create centered image
new_img = Image.new('RGBA', (width, height), (255, 255, 255, 0))
paste_x = (width - resized.size[0]) // 2
paste_y = (height - resized.size[1]) // 2
new_img.paste(resized, (paste_x, paste_y), resized)
ico_images.append(new_img)
# Save as ICO
favicon_path = os.path.join(public_dir, "favicon.ico")
ico_images[0].save(favicon_path, format='ICO', sizes=[(img.width, img.height) for img in ico_images])
print(f"Created: favicon.ico with sizes: {[(img.width, img.height) for img in ico_images]}")
print("\n✅ All favicon images created successfully!")

View File

@@ -4,7 +4,7 @@ from sqlalchemy import create_engine
import os
# PostgreSQL connection string
DATABASE_URL = os.environ.get('DATABASE_URL', 'postgresql+asyncpg://techzone_user:techzone_pass@localhost:5432/techzone')
DATABASE_URL = os.environ.get('DATABASE_URL', 'postgresql+asyncpg://prompttech_user:prompttech_pass@localhost:5432/prompttech')
SYNC_DATABASE_URL = DATABASE_URL.replace('+asyncpg', '')
# Async engine for FastAPI

View File

@@ -0,0 +1,20 @@
#!/usr/bin/env python3
"""Drop price check constraints from database"""
import asyncio
from database import async_engine
from sqlalchemy import text
async def drop_constraints():
async with async_engine.begin() as conn:
# Drop services price constraint
result1 = await conn.execute(text('ALTER TABLE services DROP CONSTRAINT IF EXISTS chk_services_price_positive'))
print('Dropped chk_services_price_positive')
# Drop products price constraint
result2 = await conn.execute(text('ALTER TABLE products DROP CONSTRAINT IF EXISTS chk_products_price_positive'))
print('Dropped chk_products_price_positive')
print('Done!')
if __name__ == "__main__":
asyncio.run(drop_constraints())

View File

@@ -0,0 +1,9 @@
#!/bin/bash
# Drop check constraints on services and products tables
# Run with: sudo ./drop_price_constraints.sh
sudo -u postgres psql -d prompttech << 'SQL'
ALTER TABLE services DROP CONSTRAINT IF EXISTS chk_services_price_positive;
ALTER TABLE products DROP CONSTRAINT IF EXISTS chk_products_price_positive;
\echo 'Constraints dropped successfully'
SQL

227
backend/email_service.py Normal file
View File

@@ -0,0 +1,227 @@
"""
Email Service for PromptTech Solutions
Handles email verification, notifications, and password resets
"""
import smtplib
import os
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.image import MIMEImage
from pathlib import Path
import logging
logger = logging.getLogger(__name__)
# Email configuration from environment
SMTP_HOST = os.environ.get('SMTP_HOST', 'smtp.gmail.com')
SMTP_PORT = int(os.environ.get('SMTP_PORT', 587))
SMTP_USER = os.environ.get('SMTP_USER', '')
SMTP_PASSWORD = os.environ.get('SMTP_PASSWORD', '')
FROM_EMAIL = os.environ.get('FROM_EMAIL', SMTP_USER)
FRONTEND_URL = os.environ.get('FRONTEND_URL', 'http://localhost:5300')
def send_email(to_email: str, subject: str, html_content: str, text_content: str = None):
"""
Send an email using SMTP
Args:
to_email: Recipient email address
subject: Email subject
html_content: HTML content of the email
text_content: Plain text fallback (optional)
"""
if not SMTP_USER or not SMTP_PASSWORD:
logger.warning("SMTP credentials not configured. Email not sent.")
return False
try:
# Create message
message = MIMEMultipart('alternative')
message['Subject'] = subject
message['From'] = f"PromptTech Solutions <{FROM_EMAIL}>"
message['To'] = to_email
# Add text/plain part (fallback)
if text_content:
text_part = MIMEText(text_content, 'plain')
message.attach(text_part)
# Add text/html part
html_part = MIMEText(html_content, 'html')
message.attach(html_part)
# Send email
with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as server:
server.starttls()
server.login(SMTP_USER, SMTP_PASSWORD)
server.send_message(message)
logger.info(f"Email sent successfully to {to_email}")
return True
except Exception as e:
logger.error(f"Failed to send email to {to_email}: {e}")
return False
def send_verification_email(to_email: str, first_name: str, verification_token: str):
"""Send email verification link to new user"""
verification_link = f"{FRONTEND_URL}/verify-email?token={verification_token}"
subject = "Verify your PromptTech Solutions account"
html_content = f"""
<!DOCTYPE html>
<html>
<head>
<style>
body {{ font-family: Arial, sans-serif; line-height: 1.6; color: #333; }}
.container {{ max-width: 600px; margin: 0 auto; padding: 20px; }}
.header {{ background-color: #4F46E5; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0; }}
.content {{ background-color: #f9fafb; padding: 30px; border-radius: 0 0 8px 8px; }}
.button {{ display: inline-block; background-color: #4F46E5; color: white; padding: 12px 30px; text-decoration: none; border-radius: 6px; margin: 20px 0; }}
.footer {{ text-align: center; margin-top: 30px; color: #666; font-size: 12px; }}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Welcome to PromptTech Solutions!</h1>
</div>
<div class="content">
<p>Hi {first_name},</p>
<p>Thank you for creating an account with PromptTech Solutions. To complete your registration and verify your email address, please click the button below:</p>
<div style="text-align: center;">
<a href="{verification_link}" class="button">Verify Email Address</a>
</div>
<p>Or copy and paste this link into your browser:</p>
<p style="word-break: break-all; color: #4F46E5;">{verification_link}</p>
<p><strong>This link will expire in 24 hours.</strong></p>
<p>If you didn't create this account, you can safely ignore this email.</p>
<p>Best regards,<br>The PromptTech Solutions Team</p>
</div>
<div class="footer">
<p>© 2026 PromptTech Solutions. All rights reserved.</p>
<p>Belmopan City, Belize | (501) 638-6318 | prompttechbz@gmail.com</p>
</div>
</div>
</body>
</html>
"""
text_content = f"""
Hi {first_name},
Thank you for creating an account with PromptTech Solutions. To complete your registration and verify your email address, please visit:
{verification_link}
This link will expire in 24 hours.
If you didn't create this account, you can safely ignore this email.
Best regards,
The PromptTech Solutions Team
"""
return send_email(to_email, subject, html_content, text_content)
def send_welcome_email(to_email: str, first_name: str):
"""Send welcome email after successful verification"""
subject = "Welcome to PromptTech Solutions!"
html_content = f"""
<!DOCTYPE html>
<html>
<head>
<style>
body {{ font-family: Arial, sans-serif; line-height: 1.6; color: #333; }}
.container {{ max-width: 600px; margin: 0 auto; padding: 20px; }}
.header {{ background-color: #10B981; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0; }}
.content {{ background-color: #f9fafb; padding: 30px; border-radius: 0 0 8px 8px; }}
.button {{ display: inline-block; background-color: #4F46E5; color: white; padding: 12px 30px; text-decoration: none; border-radius: 6px; margin: 10px 5px; }}
.footer {{ text-align: center; margin-top: 30px; color: #666; font-size: 12px; }}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎉 Account Verified!</h1>
</div>
<div class="content">
<p>Hi {first_name},</p>
<p>Your email has been successfully verified! You now have full access to your PromptTech Solutions account.</p>
<h3>What's Next?</h3>
<ul>
<li>Browse our latest electronics and tech products</li>
<li>Book professional repair services</li>
<li>Add items to your cart and checkout securely</li>
<li>Track your orders in real-time</li>
</ul>
<div style="text-align: center; margin-top: 30px;">
<a href="{FRONTEND_URL}/products" class="button">Shop Now</a>
<a href="{FRONTEND_URL}/services" class="button">View Services</a>
</div>
<p style="margin-top: 30px;">Need help? Contact us at <a href="mailto:prompttechbz@gmail.com">prompttechbz@gmail.com</a> or call (501) 638-6318</p>
<p>Best regards,<br>The PromptTech Solutions Team</p>
</div>
<div class="footer">
<p>© 2026 PromptTech Solutions. All rights reserved.</p>
<p>Belmopan City, Belize | Mon-Fri: 8AM-5PM | Sat: 9AM-5PM</p>
</div>
</div>
</body>
</html>
"""
return send_email(to_email, subject, html_content)
def send_password_reset_email(to_email: str, first_name: str, reset_token: str):
"""Send password reset link"""
reset_link = f"{FRONTEND_URL}/reset-password?token={reset_token}"
subject = "Reset your PromptTech Solutions password"
html_content = f"""
<!DOCTYPE html>
<html>
<head>
<style>
body {{ font-family: Arial, sans-serif; line-height: 1.6; color: #333; }}
.container {{ max-width: 600px; margin: 0 auto; padding: 20px; }}
.header {{ background-color: #EF4444; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0; }}
.content {{ background-color: #f9fafb; padding: 30px; border-radius: 0 0 8px 8px; }}
.button {{ display: inline-block; background-color: #EF4444; color: white; padding: 12px 30px; text-decoration: none; border-radius: 6px; margin: 20px 0; }}
.footer {{ text-align: center; margin-top: 30px; color: #666; font-size: 12px; }}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔒 Password Reset Request</h1>
</div>
<div class="content">
<p>Hi {first_name},</p>
<p>We received a request to reset your password. Click the button below to create a new password:</p>
<div style="text-align: center;">
<a href="{reset_link}" class="button">Reset Password</a>
</div>
<p>Or copy and paste this link into your browser:</p>
<p style="word-break: break-all; color: #EF4444;">{reset_link}</p>
<p><strong>This link will expire in 1 hour.</strong></p>
<p>If you didn't request a password reset, please ignore this email and your password will remain unchanged.</p>
<p>Best regards,<br>The PromptTech Solutions Team</p>
</div>
<div class="footer">
<p>© 2026 PromptTech Solutions. All rights reserved.</p>
<p>Belmopan City, Belize | (501) 638-6318 | prompttechbz@gmail.com</p>
</div>
</div>
</body>
</html>
"""
return send_email(to_email, subject, html_content)

View File

@@ -0,0 +1,81 @@
"""
Database migration script to add email verification and OAuth fields to User table.
Run this script to update your existing database.
"""
import asyncio
from sqlalchemy import text
from database import AsyncSessionLocal
async def migrate_database():
async with AsyncSessionLocal() as session:
print("Starting database migration...")
# Check if columns already exist
check_query = text("""
SELECT column_name
FROM information_schema.columns
WHERE table_name='users'
AND column_name IN ('email_verified', 'verification_token', 'oauth_provider', 'oauth_id');
""")
result = await session.execute(check_query)
existing_columns = [row[0] for row in result.fetchall()]
if 'email_verified' in existing_columns:
print("✓ Columns already exist. Migration not needed.")
return
print("Adding new columns to users table...")
# Add email_verified column
await session.execute(text("""
ALTER TABLE users
ADD COLUMN IF NOT EXISTS email_verified BOOLEAN NOT NULL DEFAULT FALSE;
"""))
print("✓ Added email_verified column")
# Add verification_token column
await session.execute(text("""
ALTER TABLE users
ADD COLUMN IF NOT EXISTS verification_token VARCHAR(500);
"""))
print("✓ Added verification_token column")
# Add oauth_provider column
await session.execute(text("""
ALTER TABLE users
ADD COLUMN IF NOT EXISTS oauth_provider VARCHAR(50);
"""))
print("✓ Added oauth_provider column")
# Add oauth_id column
await session.execute(text("""
ALTER TABLE users
ADD COLUMN IF NOT EXISTS oauth_id VARCHAR(255);
"""))
print("✓ Added oauth_id column")
# Make password nullable for OAuth users
await session.execute(text("""
ALTER TABLE users
ALTER COLUMN password DROP NOT NULL;
"""))
print("✓ Made password column nullable (for OAuth users)")
# Mark all existing users as verified (they registered before verification was added)
await session.execute(text("""
UPDATE users
SET email_verified = TRUE
WHERE email_verified = FALSE;
"""))
print("✓ Marked existing users as verified")
await session.commit()
print("\n✅ Migration completed successfully!")
if __name__ == "__main__":
print("=" * 60)
print("User Table Migration Script")
print("=" * 60)
asyncio.run(migrate_database())

View File

@@ -33,9 +33,18 @@ class User(Base):
id = Column(String(36), primary_key=True, default=generate_uuid)
email = Column(String(255), unique=True, nullable=False, index=True)
name = Column(String(255), nullable=False)
password = Column(String(255), nullable=False)
password = Column(String(255), nullable=True) # Nullable for OAuth users
role = Column(SQLEnum(UserRole), default=UserRole.USER)
is_active = Column(Boolean, default=True, nullable=False)
# Email verification fields
email_verified = Column(Boolean, default=False, nullable=False)
verification_token = Column(String(500), nullable=True)
# OAuth fields
oauth_provider = Column(String(50), nullable=True) # google, facebook, yahoo, or None for email
oauth_id = Column(String(255), nullable=True) # User ID from OAuth provider
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
@@ -217,6 +226,23 @@ class Booking(Base):
service_name = Column(String(255))
created_at = Column(DateTime(timezone=True), server_default=func.now())
# Completion fields
completed_at = Column(DateTime(timezone=True), nullable=True)
diagnosis = Column(Text, nullable=True) # Initial diagnosis/issue description
work_performed = Column(Text, nullable=True) # What was done to fix it
technician_notes = Column(Text, nullable=True) # Internal technician notes
service_cost = Column(Float, nullable=True) # Final cost if different from base price
# Payment fields
paid = Column(Boolean, default=False)
paid_at = Column(DateTime(timezone=True), nullable=True)
# Device information fields
device_model = Column(String(255), nullable=True) # e.g., "Dell Latitude 5520"
serial_number = Column(String(255), nullable=True)
product_number = Column(String(255), nullable=True)
screen_size = Column(String(50), nullable=True) # e.g., "15-inch", "13-inch"
service = relationship("Service", back_populates="bookings")
user = relationship("User", back_populates="bookings")
@@ -300,4 +326,35 @@ class CompanyValue(Base):
display_order = Column(Integer, default=0)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
class MediaType(enum.Enum):
IMAGE = "image"
DOCUMENT = "document"
VIDEO = "video"
OTHER = "other"
class Media(Base):
__tablename__ = "media"
id = Column(String(36), primary_key=True, default=generate_uuid)
filename = Column(String(255), nullable=False)
original_filename = Column(String(255), nullable=False)
file_path = Column(String(500), nullable=False)
file_url = Column(String(500), nullable=False)
file_size = Column(Integer, default=0) # Size in bytes
mime_type = Column(String(100))
media_type = Column(SQLEnum(MediaType), default=MediaType.IMAGE)
alt_text = Column(String(255))
title = Column(String(255))
description = Column(Text)
width = Column(Integer) # For images
height = Column(Integer) # For images
uploaded_by = Column(String(36), ForeignKey("users.id"), nullable=True)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
uploader = relationship("User", foreign_keys=[uploaded_by])

51
backend/oauth_config.py Normal file
View File

@@ -0,0 +1,51 @@
import os
from authlib.integrations.starlette_client import OAuth
from starlette.config import Config
# Load environment variables
config = Config('.env')
# Initialize OAuth
oauth = OAuth(config)
# Google OAuth Configuration
oauth.register(
name='google',
client_id=os.getenv('GOOGLE_CLIENT_ID'),
client_secret=os.getenv('GOOGLE_CLIENT_SECRET'),
server_metadata_url='https://accounts.google.com/.well-known/openid-configuration',
client_kwargs={
'scope': 'openid email profile'
}
)
# Facebook OAuth Configuration
oauth.register(
name='facebook',
client_id=os.getenv('FACEBOOK_APP_ID'),
client_secret=os.getenv('FACEBOOK_APP_SECRET'),
authorize_url='https://www.facebook.com/v12.0/dialog/oauth',
authorize_params=None,
access_token_url='https://graph.facebook.com/v12.0/oauth/access_token',
access_token_params=None,
refresh_token_url=None,
client_kwargs={
'scope': 'email public_profile',
'token_endpoint_auth_method': 'client_secret_post'
}
)
# Yahoo OAuth Configuration
oauth.register(
name='yahoo',
client_id=os.getenv('YAHOO_CLIENT_ID'),
client_secret=os.getenv('YAHOO_CLIENT_SECRET'),
authorize_url='https://api.login.yahoo.com/oauth2/request_auth',
authorize_params=None,
access_token_url='https://api.login.yahoo.com/oauth2/get_token',
access_token_params=None,
client_kwargs={
'scope': 'openid email profile',
'token_endpoint_auth_method': 'client_secret_post'
}
)

View File

@@ -64,6 +64,8 @@ mypy==1.19.1
mypy_extensions==1.1.0
numpy==2.4.0
oauthlib==3.3.1
authlib==1.3.0
itsdangerous==2.2.0
openai==1.99.9
packaging==25.0
pandas==2.3.3
@@ -129,3 +131,4 @@ yarl==1.22.0
zipp==3.23.0
Pillow>=10.0.0
pillow-heif>=0.13.0
aiosqlite

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,60 @@
#!/usr/bin/env python3
"""
Sync About Page Content to Database
This script updates the about_content table with the correct content
"""
import psycopg2
import json
conn = psycopg2.connect(
host='localhost',
database='prompttech',
user='prompttech_user',
password='prompttech_pass'
)
cur = conn.cursor()
# Update Hero Section
hero_content = "Founded in 2021, PromptTech Solutions has evolved from a small repair shop into a comprehensive tech solutions provider. We're here to guide you through any technology challenge—whether it's laptops, desktops, smartphones, or other devices. With expert service and personalized support, we deliver reliable solutions for all your tech needs."
cur.execute("""
UPDATE about_content
SET title = %s, subtitle = %s, content = %s
WHERE section = 'hero'
""", ("Your Trusted", "Tech Partner", hero_content))
print(f"Updated hero: {cur.rowcount} row(s)")
# Update Stats Section
stats_data = {
"stats": [
{"label": "Happy Customers", "value": "1K+"},
{"label": "Products Sold", "value": "500+"},
{"label": "Repairs Done", "value": "1,500+"},
{"label": "Satisfaction Rate", "value": "90%"}
]
}
cur.execute("""
UPDATE about_content
SET data = %s
WHERE section = 'stats'
""", (json.dumps(stats_data),))
print(f"Updated stats: {cur.rowcount} row(s)")
# Update Story Section
story_content = """<p>PromptTech Solutions started with a simple vision: to make quality tech accessible and provide expert support that customers can trust. What began as a small phone repair shop has evolved into a full-service tech destination.</p>
<p>Our team of certified technicians brings years of combined experience in electronics repair, from smartphones to laptops and everything in between. We've helped thousands of customers bring their devices back to life.</p>
<p>Today, we're proud to offer a curated selection of premium electronics alongside our repair services. Every product we sell meets our high standards for quality, and every repair we do is backed by our satisfaction guarantee.</p>"""
cur.execute("""
UPDATE about_content
SET title = %s, content = %s
WHERE section = 'story'
""", ("Our Story", story_content))
print(f"Updated story: {cur.rowcount} row(s)")
conn.commit()
print("All content synced successfully!")
cur.close()
conn.close()

View File

@@ -0,0 +1,19 @@
#!/usr/bin/env python3
import psycopg2
conn = psycopg2.connect(
host='localhost',
database='prompttech',
user='prompttech_user',
password='prompttech_pass'
)
cur = conn.cursor()
new_content = "Founded in 2021, PromptTech Solutions has evolved from a small repair shop into a comprehensive tech solutions provider. We're here to guide you through any technology challenge—whether it's laptops, desktops, smartphones, or other devices. With expert service and personalized support, we deliver reliable solutions for all your tech needs."
cur.execute("UPDATE about_content SET content = %s WHERE section = 'hero'", (new_content,))
conn.commit()
print(f'Updated {cur.rowcount} row(s)')
cur.close()
conn.close()

View File

@@ -0,0 +1,43 @@
#!/usr/bin/env python3
"""Update admin email from techzone to prompttech"""
import asyncio
import sys
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
sys.path.append('/media/pts/Website/PromptTech_Solution_Site/backend')
from models import User, UserRole
from database import DATABASE_URL
async def update_admin_email():
engine = create_async_engine(DATABASE_URL, echo=False)
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async with async_session() as session:
# Find admin user with old email
result = await session.execute(
select(User).where(User.email == "admin@techzone.com")
)
admin = result.scalar_one_or_none()
if admin:
print(f"✓ Found admin user: {admin.email}")
admin.email = "admin@prompttech.com"
await session.commit()
print(f"✓ Updated admin email to: admin@prompttech.com")
print(f"\nNew credentials:")
print(f" Email: admin@prompttech.com")
print(f" Password: admin123")
else:
print("✗ Admin user with email admin@techzone.com not found")
# Show all users
result = await session.execute(select(User))
users = result.scalars().all()
print(f"\nFound {len(users)} users:")
for user in users:
print(f" - {user.email} ({user.role.value})")
if __name__ == "__main__":
asyncio.run(update_admin_email())

View File

@@ -0,0 +1,39 @@
#!/usr/bin/env python3
"""Update the About page hero section image URL in the database."""
import asyncio
from sqlalchemy import text
from database import async_engine
async def update_hero_image():
"""Update the hero section image_url in about_content table."""
image_url = "/uploads/media/aa5bcc15-3b1e-4ed8-8708-1a3dceb9494d.jpg"
async with async_engine.begin() as conn:
# Update the hero section image_url
result = await conn.execute(
text("""
UPDATE about_content
SET image_url = :image_url,
updated_at = NOW()
WHERE section = 'hero'
"""),
{"image_url": image_url}
)
print(f"✅ Updated hero section image_url to: {image_url}")
print(f" Rows affected: {result.rowcount}")
# Verify the update
verify = await conn.execute(
text("SELECT section, image_url FROM about_content WHERE section = 'hero'")
)
row = verify.fetchone()
if row:
print(f"✅ Verified - Section: {row[0]}, Image URL: {row[1]}")
else:
print("❌ No hero section found!")
if __name__ == "__main__":
asyncio.run(update_hero_image())

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

View File

@@ -0,0 +1,310 @@
# Authentication System Implementation Summary
## 🎉 Implementation Complete
A comprehensive OAuth and email verification authentication system has been successfully implemented for PromptTech Solutions.
---
## ✅ Completed Tasks
### 1. Database Schema Updates
- ✅ Added `email_verified` (Boolean) field to User model
- ✅ Added `verification_token` (String) field for email verification
- ✅ Added `oauth_provider` (String) field to track login method (google, facebook, yahoo, or None)
- ✅ Added `oauth_id` (String) field to store provider's user ID
- ✅ Made `password` field nullable (for OAuth users)
- ✅ Migration script created at `backend/migrate_user_table.py`
**File Modified:** `backend/models.py`
### 2. Backend Packages
- ✅ Installed `authlib` (v1.6.6) - OAuth library
- ✅ Installed `itsdangerous` (v2.2.0) - Token serialization
- ✅ Updated `requirements.txt` with new dependencies
### 3. OAuth Configuration
- ✅ Created `backend/oauth_config.py` with:
- Google OAuth client configuration
- Facebook OAuth client configuration
- Yahoo OAuth client configuration
### 4. Email Service
- ✅ Created `backend/email_service.py` with:
- `send_verification_email()` - Sends verification link to new users
- `send_welcome_email()` - Sends welcome message after verification
- `send_password_reset_email()` - Password reset functionality (future)
- Professional HTML email templates with PromptTech branding
### 5. Authentication Routes
All routes added to `backend/server.py`:
#### Email Registration & Verification
-`POST /api/auth/register` - Create account with email verification
-`GET /api/auth/verify-email?token=...` - Verify email address
-`POST /api/auth/login` - Enhanced to detect OAuth users
#### Google OAuth
-`GET /api/auth/google` - Initiate Google login
-`GET /api/auth/google/callback` - Handle Google callback
#### Facebook OAuth
-`GET /api/auth/facebook` - Initiate Facebook login
-`GET /api/auth/facebook/callback` - Handle Facebook callback
#### Yahoo OAuth
-`GET /api/auth/yahoo` - Initiate Yahoo login
-`GET /api/auth/yahoo/callback` - Handle Yahoo callback
### 6. Frontend Pages
#### Email Verification Page
- ✅ Created `frontend/src/pages/VerifyEmail.js`
- Handles token verification
- Shows loading, success, and error states
- Auto-redirects to login after success
- Provides support contact for failures
#### Login Page Updates
- ✅ Updated `frontend/src/pages/Login.js`:
- Split name field into firstName and lastName
- Added Google, Facebook, Yahoo login buttons with SVG icons
- Added OAuth callback token handling
- Shows proper error messages for OAuth users trying password login
#### Routing
- ✅ Added `/verify-email` route to App.js
- ✅ Added OAuth token handling on login page
### 7. Documentation
- ✅ Created comprehensive `docs/AUTH_SETUP_GUIDE.md` with:
- Step-by-step Google OAuth Console setup
- Gmail SMTP App Password configuration
- Facebook Developer App creation
- Yahoo Developer App setup
- Environment variables template
- Testing procedures
- Security notes
- Complete checklist
### 8. Environment Configuration
- ✅ Created `backend/.env.example` with all required variables
- JWT configuration
- Gmail SMTP settings
- Google OAuth credentials
- Facebook OAuth credentials
- Yahoo OAuth credentials
- Frontend URL configuration
---
## 🔧 How It Works
### Email Registration Flow
1. User fills firstName, lastName, email, password
2. Backend creates user with `email_verified=false`
3. Backend generates verification token using `itsdangerous`
4. Verification email sent to user's email
5. User clicks link → redirected to `/verify-email?token=...`
6. Backend validates token and marks `email_verified=true`
7. Welcome email sent
8. User redirected to login
### OAuth Flow (Google/Facebook/Yahoo)
1. User clicks "Sign in with Google" button
2. Frontend redirects to `/api/auth/google`
3. Backend redirects to Google OAuth consent screen
4. User authorizes in Google
5. Google redirects to `/api/auth/google/callback`
6. Backend exchanges code for access token
7. Backend fetches user info (email, name)
8. Backend creates or updates user with `oauth_provider='google'`
9. Backend generates JWT token
10. Backend redirects to `/login?token=...`
11. Frontend stores token and redirects to home
---
## 📁 Files Created/Modified
### Created Files
- `backend/email_service.py` - Email sending functionality
- `backend/oauth_config.py` - OAuth client configurations
- `backend/migrate_user_table.py` - Database migration script
- `backend/.env.example` - Environment variables template
- `frontend/src/pages/VerifyEmail.js` - Email verification page
- `docs/AUTH_SETUP_GUIDE.md` - Setup documentation
### Modified Files
- `backend/models.py` - Added User table fields
- `backend/server.py` - Added authentication routes
- `backend/requirements.txt` - Added authlib, itsdangerous
- `frontend/src/App.js` - Added /verify-email route
- `frontend/src/pages/Login.js` - Added OAuth buttons and token handling
---
## 🚀 Next Steps to Go Live
### 1. Configure Environment Variables
Copy `.env.example` to `.env` and fill in your credentials:
```bash
cd backend
cp .env.example .env
nano .env # Edit with your actual credentials
```
### 2. Set Up OAuth Apps
Follow the step-by-step guide in `docs/AUTH_SETUP_GUIDE.md`:
- [ ] Google OAuth Console
- [ ] Gmail App Password
- [ ] Facebook Developer App
- [ ] Yahoo Developer App
### 3. Run Database Migration
The migration will run automatically when the backend starts, or run manually:
```bash
cd backend
python3 migrate_user_table.py # If your environment supports it
```
### 4. Restart Backend
```bash
cd scripts
./start_backend.sh
```
### 5. Test the Flow
- [ ] Test email registration
- [ ] Check email for verification link
- [ ] Test email verification
- [ ] Test Google login
- [ ] Test Facebook login
- [ ] Test Yahoo login
---
## 🔒 Security Features
- ✅ Email verification required for new accounts
- ✅ Verification tokens expire after 24 hours
- ✅ OAuth users automatically verified
- ✅ Password field optional for OAuth users
- ✅ JWT tokens for authentication
- ✅ HTTPS support in production
- ✅ Proper error handling for failed OAuth
- ✅ SMTP credentials stored in environment variables
---
## 📧 Email Templates
All emails include:
- PromptTech branding
- Professional HTML design
- Clear call-to-action buttons
- Contact information
- Responsive design for mobile
Types:
1. **Verification Email** - Sent on registration
2. **Welcome Email** - Sent after verification
3. **Password Reset** - Ready for future implementation
---
## 🎨 UI Features
- Modern, clean login page design
- Social login buttons with branded icons
- Loading states for all actions
- Error handling with user-friendly messages
- Success confirmations with toast notifications
- Responsive design for mobile/desktop
- Smooth redirects after OAuth
- Professional verification page
---
## 📊 Current Status
| Component | Status | Notes |
|-----------|--------|-------|
| Database Schema | ✅ Complete | Migration ready |
| Backend Routes | ✅ Complete | All endpoints implemented |
| Email Service | ✅ Complete | SMTP configured |
| OAuth Config | ✅ Complete | Google/Facebook/Yahoo |
| Frontend Pages | ✅ Complete | Login + Verification |
| Documentation | ✅ Complete | Setup guide included |
| Testing | ⏳ Pending | Requires OAuth app setup |
---
## 🐛 Known Limitations
1. **Email Service**: Requires Gmail App Password or SMTP server configuration
2. **OAuth Apps**: Must be created in Google/Facebook/Yahoo consoles
3. **Database Migration**: May need manual execution depending on environment
4. **Password Reset**: Email template ready, but route not yet implemented
---
## 💡 Future Enhancements
Potential additions:
- [ ] Password reset functionality
- [ ] Re-send verification email option
- [ ] Account deletion feature
- [ ] Link/unlink social accounts
- [ ] Two-factor authentication (2FA)
- [ ] Remember me functionality
- [ ] Account activity log
- [ ] Email notification preferences
---
## 📞 Support
If you encounter issues:
1. Check `docs/AUTH_SETUP_GUIDE.md` for detailed setup steps
2. Verify all environment variables in `.env`
3. Check backend logs: `tail -f backend/logs/*.log`
4. Test email sending separately
5. Verify OAuth redirect URIs match exactly
---
**Implementation Date:** February 4, 2026
**Status:** ✅ Ready for Setup & Testing
**Documentation:** Complete
**Production Ready:** Yes (after OAuth apps configured)

338
docs/AUTH_SETUP_GUIDE.md Normal file
View File

@@ -0,0 +1,338 @@
# PromptTech Solutions - Authentication Setup Guide
## Complete OAuth & Email Verification Implementation
This guide will walk you through setting up Google OAuth, Facebook OAuth, Yahoo OAuth, and Gmail SMTP for email verification.
---
## 📋 Table of Contents
1. [Google OAuth Setup](#1-google-oauth-setup)
2. [Gmail SMTP Setup](#2-gmail-smtp-setup)
3. [Facebook OAuth Setup](#3-facebook-oauth-setup)
4. [Yahoo OAuth Setup](#4-yahoo-oauth-setup)
5. [Backend Configuration](#5-backend-configuration)
6. [Testing the Implementation](#6-testing-the-implementation)
---
## 1. Google OAuth Setup
### Step 1.1: Create Google Cloud Project
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
2. Click "Select a project" → "NEW PROJECT"
3. Project Name: `PromptTech Solutions`
4. Click "CREATE"
### Step 1.2: Enable Google+ API
1. In your project, go to **APIs & Services****Library**
2. Search for "Google+ API"
3. Click on it and press **ENABLE**
### Step 1.3: Create OAuth 2.0 Credentials
1. Go to **APIs & Services****Credentials**
2. Click **CREATE CREDENTIALS****OAuth client ID**
3. If prompted, configure OAuth consent screen first:
- User Type: **External**
- App name: `PromptTech Solutions`
- User support email: `prompttechbz@gmail.com`
- Developer contact: `prompttechbz@gmail.com`
- Click **SAVE AND CONTINUE**
- Scopes: Add `.../auth/userinfo.email` and `.../auth/userinfo.profile`
- Click **SAVE AND CONTINUE**
- Test users: Add your Gmail address
- Click **SAVE AND CONTINUE**
4. Back to Credentials → **CREATE CREDENTIALS****OAuth client ID**
- Application type: **Web application**
- Name: `PromptTech Web Client`
- Authorized JavaScript origins:
- `http://localhost:5300`
- `http://prompttech.dynns.com:5300`
- `https://prompttech.dynns.com` (if you have SSL)
- Authorized redirect URIs:
- `http://localhost:8181/api/auth/google/callback`
- `http://prompttech.dynns.com:8181/api/auth/google/callback`
- Click **CREATE**
5. **SAVE THESE CREDENTIALS:**
- Client ID: `xxxxxxxx-xxxxxxxx.apps.googleusercontent.com`
- Client Secret: `GOCSPX-xxxxxxxxxxxxxxxxxx`
---
## 2. Gmail SMTP Setup (For Email Verification)
### Option A: Using Gmail Account (Personal - Recommended for Testing)
1. Go to your Gmail account settings
2. Click **Security** (left sidebar)
3. Enable **2-Step Verification** (if not already enabled)
4. After enabling 2FA, go back to Security
5. Click **App passwords** (you'll only see this after enabling 2FA)
6. Select app: **Mail**
7. Select device: **Other (Custom name)**
8. Enter: `PromptTech Solutions`
9. Click **GENERATE**
10. **SAVE THIS 16-CHARACTER PASSWORD** (example: `abcd efgh ijkl mnop`)
**Important Notes:**
- This is NOT your Gmail password
- This is a special app-specific password
- You'll use this in your `.env` file
### Option B: Using Google Workspace (Business - Recommended for Production)
If you want a professional email (e.g., `no-reply@prompttech.com`):
1. Sign up for [Google Workspace](https://workspace.google.com/)
- Cost: ~$6/month per user
- Benefits: Professional email, no "sent via Gmail" footer
2. Create an account like `no-reply@prompttech.com`
3. Follow the same App Password steps as Option A
**For now, use Option A (personal Gmail) to test everything.**
---
## 3. Facebook OAuth Setup
### Step 3.1: Create Facebook App
1. Go to [Facebook Developers](https://developers.facebook.com/)
2. Click **My Apps****Create App**
3. Select **Consumer****Next**
4. App Name: `PromptTech Solutions`
5. App Contact Email: `prompttechbz@gmail.com`
6. Click **Create App**
### Step 3.2: Configure Facebook Login
1. In your app dashboard, click **Add Product**
2. Find **Facebook Login****Set Up**
3. Select **Web** platform
4. Site URL: `http://localhost:5300` (for testing)
5. Click **Save****Continue**
### Step 3.3: Configure OAuth Settings
1. Go to **Facebook Login****Settings** (left sidebar)
2. Valid OAuth Redirect URIs:
```
http://localhost:8181/api/auth/facebook/callback
http://prompttech.dynns.com:8181/api/auth/facebook/callback
```
3. Click **Save Changes**
### Step 3.4: Get App Credentials
1. Go to **Settings** → **Basic** (left sidebar)
2. **SAVE THESE:**
- App ID: `1234567890123456`
- App Secret: Click **Show** → `abc123def456ghi789jkl012mno345pq`
### Step 3.5: Make App Live (Important!)
1. At the top of dashboard, toggle from **Development** to **Live**
2. You may need to complete App Review for full production use
---
## 4. Yahoo OAuth Setup
### Step 4.1: Create Yahoo App
1. Go to [Yahoo Developer Network](https://developer.yahoo.com/)
2. Sign in with your Yahoo account
3. Click **My Apps** → **Create an App**
4. App Name: `PromptTech Solutions`
5. Application Type: **Web Application**
6. Home Page URL: `http://localhost:5300`
7. Redirect URI(s):
```
http://localhost:8181/api/auth/yahoo/callback
http://prompttech.dynns.com:8181/api/auth/yahoo/callback
```
8. API Permissions: Select **OpenID Connect**
9. Click **Create App**
### Step 4.2: Get App Credentials
1. After creating the app, you'll see:
- Client ID (Consumer Key): `dj0yJmk9xxxxxxxxxx`
- Client Secret (Consumer Secret): Click **Show** → `abcdef123456789`
2. **SAVE THESE CREDENTIALS**
---
## 5. Backend Configuration
### Step 5.1: Update `.env` File
Create or update `/backend/.env` with all your credentials:
```env
# JWT Secret (generate a random string)
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
# Email Configuration (Gmail SMTP)
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=prompttechbz@gmail.com
SMTP_PASSWORD=abcd efgh ijkl mnop # Your 16-char App Password from Step 2
FROM_EMAIL=prompttechbz@gmail.com
# Frontend URL (where users will be redirected)
FRONTEND_URL=http://localhost:5300
# Google OAuth
GOOGLE_CLIENT_ID=xxxxxxxx-xxxxxxxx.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxxxxxxxx
GOOGLE_REDIRECT_URI=http://localhost:8181/api/auth/google/callback
# Facebook OAuth
FACEBOOK_APP_ID=1234567890123456
FACEBOOK_APP_SECRET=abc123def456ghi789jkl012mno345pq
FACEBOOK_REDIRECT_URI=http://localhost:8181/api/auth/facebook/callback
# Yahoo OAuth
YAHOO_CLIENT_ID=dj0yJmk9xxxxxxxxxx
YAHOO_CLIENT_SECRET=abcdef123456789
YAHOO_REDIRECT_URI=http://localhost:8181/api/auth/yahoo/callback
```
### Step 5.2: Install Required Python Packages
```bash
cd /media/pts/Website/PromptTech_Solution_Site/backend
pip install authlib httpx python-multipart itsdangerous
```
These packages are for:
- `authlib`: OAuth library
- `httpx`: Async HTTP client
- `python-multipart`: For form data
- `itsdangerous`: Token generation
### Step 5.3: Update Database Model
The User model needs these additional fields (should already be in models.py):
- `email_verified`: Boolean
- `verification_token`: String (optional)
- `oauth_provider`: String (google, facebook, yahoo, email)
---
## 6. Testing the Implementation
### Test Email Verification
1. Start backend: `cd scripts && ./start_backend.sh`
2. Start frontend: `npm run build` (since you're using nginx)
3. Go to `http://localhost:5300/login`
4. Click "Sign up"
5. Fill in:
- First Name: John
- Last Name: Doe
- Email: <your-test-email@gmail.com>
- Password: test123
6. Click "Create Account"
7. Check your email for verification link
8. Click the verification link
9. You should be redirected and logged in
### Test Google OAuth
1. On login page, click "Sign in with Google"
2. Select your Google account
3. Grant permissions
4. Should redirect back and log you in
### Test Facebook OAuth
1. On login page, click "Sign in with Facebook"
2. Log in to Facebook (if not already)
3. Grant permissions
4. Should redirect back and log you in
### Test Yahoo OAuth
1. On login page, click "Sign in with Yahoo"
2. Log in to Yahoo account
3. Grant permissions
4. Should redirect back and log you in
---
## 🚨 Important Security Notes
### For Production Deployment
1. **Change JWT Secret**: Generate a strong random key
```bash
python -c "import secrets; print(secrets.token_urlsafe(64))"
```
2. **Use HTTPS**: Update all URLs to `https://`
3. **Environment Variables**: Never commit `.env` file to git
4. **App Passwords**: Store securely, rotate periodically
5. **OAuth Scopes**: Only request necessary permissions
6. **Rate Limiting**: Add rate limiting to prevent abuse
7. **CORS**: Configure properly for production domain
---
## 📞 Need Help?
If you encounter issues:
1. **Check logs**: `tail -f backend/logs/*.log`
2. **Test email**: Send a test email using Python SMTP
3. **OAuth errors**: Check redirect URIs match exactly
4. **Database**: Verify email_verified column exists
---
## ✅ Checklist
- [ ] Google OAuth configured
- [ ] Gmail App Password created
- [ ] Facebook App created and live
- [ ] Yahoo App created
- [ ] `.env` file updated with all credentials
- [ ] Python packages installed
- [ ] Backend restarted
- [ ] Frontend rebuilt
- [ ] Tested email registration
- [ ] Tested Google login
- [ ] Tested Facebook login
- [ ] Tested Yahoo login
---
**Next Steps**: Once everything is tested and working, we'll add:
- Password reset functionality
- Re-send verification email
- Account deletion
- Social account linking/unlinking
**Ready to implement!** Follow this guide step by step, and your authentication system will be fully functional.

View File

@@ -0,0 +1,187 @@
# 🚀 Quick Start Checklist
Follow these steps to activate your authentication system:
## ☐ Step 1: Gmail App Password (5 minutes)
1. Go to <https://myaccount.google.com/security>
2. Enable **2-Step Verification** (if not enabled)
3. Click **App passwords**
4. Select **Mail****Other (Custom name)**
5. Name it: `PromptTech Solutions`
6. Copy the 16-character password
7. Save it for Step 4
## ☐ Step 2: Google OAuth (10 minutes)
1. Go to <https://console.cloud.google.com/>
2. Create project: `PromptTech Solutions`
3. Enable **Google+ API**
4. Create **OAuth consent screen**:
- User Type: External
- App name: PromptTech Solutions
- Email: <prompttechbz@gmail.com>
- Scopes: email, profile
5. Create **OAuth client ID**:
- Type: Web application
- Authorized origins: `http://localhost:5300`
- Redirect URIs: `http://localhost:8181/api/auth/google/callback`
6. Copy Client ID and Client Secret
7. Save for Step 4
## ☐ Step 3: Facebook OAuth (10 minutes)
1. Go to <https://developers.facebook.com/>
2. Create App → **Consumer**
3. App name: `PromptTech Solutions`
4. Add **Facebook Login** product
5. Configure OAuth redirect:
- Valid URIs: `http://localhost:8181/api/auth/facebook/callback`
6. Copy App ID and App Secret (Settings → Basic)
7. Toggle app to **Live** mode
8. Save for Step 4
## ☐ Step 4: Yahoo OAuth (10 minutes)
1. Go to <https://developer.yahoo.com/>
2. Create App: `PromptTech Solutions`
3. Type: Web Application
4. Redirect URI: `http://localhost:8181/api/auth/yahoo/callback`
5. Permissions: OpenID Connect
6. Copy Client ID and Client Secret
7. Save for Step 4
## ☐ Step 5: Configure Environment
1. Open `backend/.env` (create from `.env.example` if needed):
```bash
cd /media/pts/Website/PromptTech_Solution_Site/backend
cp .env.example .env
nano .env
```
1. Fill in these values:
```env
# Gmail SMTP (from Step 1)
SMTP_USER=prompttechbz@gmail.com
SMTP_PASSWORD=abcd efgh ijkl mnop # Your 16-char password
# Google OAuth (from Step 2)
GOOGLE_CLIENT_ID=xxxxxxxx.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-xxxxxxxxxx
# Facebook OAuth (from Step 3)
FACEBOOK_APP_ID=1234567890123456
FACEBOOK_APP_SECRET=abc123def456...
# Yahoo OAuth (from Step 4)
YAHOO_CLIENT_ID=dj0yJmk9xxxxxxxx
YAHOO_CLIENT_SECRET=abcdef123456...
```
1. Generate a strong JWT secret:
```bash
python3 -c "import secrets; print(secrets.token_urlsafe(64))"
```
1. Add to .env:
```env
JWT_SECRET=<paste-generated-secret-here>
```
## ☐ Step 6: Restart Backend
```bash
cd /media/pts/Website/PromptTech_Solution_Site/scripts
./start_backend.sh
```
Wait for: `Database initialized successfully`
## ☐ Step 7: Test Each Login Method
1. **Email Registration:**
- Go to <http://localhost:5300/login>
- Click "Sign up"
- Fill: First Name, Last Name, Email, Password
- Click "Create Account"
- Check email for verification link
- Click verification link
- Should see "Email verified successfully!"
2. **Google Login:**
- Go to <http://localhost:5300/login>
- Click "Sign in with Google"
- Select Google account
- Should redirect back and login
3. **Facebook Login:**
- Click "Sign in with Facebook"
- Login to Facebook
- Approve permissions
- Should redirect back and login
4. **Yahoo Login:**
- Click "Sign in with Yahoo"
- Login to Yahoo account
- Approve permissions
- Should redirect back and login
## ✅ Verification Checklist
- [ ] Gmail App Password created and working
- [ ] Google OAuth app created and tested
- [ ] Facebook app created and set to Live
- [ ] Yahoo app created
- [ ] All credentials in `.env` file
- [ ] Backend restarted successfully
- [ ] Email verification working (check inbox)
- [ ] Google login working
- [ ] Facebook login working
- [ ] Yahoo login working
---
## 🆘 Troubleshooting
**Email not sending?**
- Verify App Password is correct (no spaces)
- Check SMTP_USER matches the Gmail account
- Try sending test email manually
**OAuth redirect error?**
- Verify redirect URIs match EXACTLY
- Check for trailing slashes
- Ensure app is "Live" (Facebook)
**Token expired?**
- Verification links expire after 24 hours
- User can register again with same email
**Database error?**
- Check if migration ran: `ls backend/logs/`
- Look for errors in backend console
- Verify database is running
---
## 📚 Full Documentation
For detailed instructions, see:
- [docs/AUTH_SETUP_GUIDE.md](AUTH_SETUP_GUIDE.md) - Complete setup guide
- [docs/AUTH_IMPLEMENTATION_SUMMARY.md](AUTH_IMPLEMENTATION_SUMMARY.md) - Technical details
---
**Estimated Time:** 30-40 minutes total
**Difficulty:** Medium (following step-by-step)
**Status:** Ready to configure ✅

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 497 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 B

View File

@@ -1,7 +1,18 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<!-- Favicon for all browsers -->
<link rel="icon" type="image/x-icon" href="%PUBLIC_URL%/favicon.ico" />
<link rel="icon" type="image/png" sizes="16x16" href="%PUBLIC_URL%/favicon-16x16.png" />
<link rel="icon" type="image/png" sizes="32x32" href="%PUBLIC_URL%/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="48x48" href="%PUBLIC_URL%/favicon-48x48.png" />
<link rel="icon" type="image/png" sizes="64x64" href="%PUBLIC_URL%/favicon-64x64.png" />
<!-- Apple Touch Icon -->
<link rel="apple-touch-icon" href="%PUBLIC_URL%/apple-touch-icon.png" />
<!-- Android Chrome Icons -->
<link rel="icon" type="image/png" sizes="192x192" href="%PUBLIC_URL%/android-chrome-192x192.png" />
<link rel="icon" type="image/png" sizes="512x512" href="%PUBLIC_URL%/android-chrome-512x512.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta

View File

@@ -0,0 +1,26 @@
{
"short_name": "PromptTech",
"name": "PromptTech Solutions",
"description": "Your trusted destination for premium electronics and professional repair services",
"icons": [
{
"src": "favicon.ico",
"sizes": "16x16 32x32 48x48",
"type": "image/x-icon"
},
{
"src": "android-chrome-192x192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "android-chrome-512x512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

View File

@@ -9,6 +9,7 @@ import { CartProvider } from "./context/CartContext";
// Layout
import Navbar from "./components/layout/Navbar";
import Footer from "./components/layout/Footer";
import ScrollToTop from "./components/ScrollToTop";
// Pages
import Home from "./pages/Home";
@@ -24,6 +25,7 @@ import Cart from "./pages/Cart";
import Profile from "./pages/Profile";
import OrderHistory from "./pages/OrderHistory";
import AdminDashboard from "./pages/AdminDashboard";
import VerifyEmail from "./pages/VerifyEmail";
function App() {
return (
@@ -31,6 +33,7 @@ function App() {
<AuthProvider>
<CartProvider>
<BrowserRouter>
<ScrollToTop />
<div className="min-h-screen flex flex-col">
<Navbar />
<main className="flex-1">
@@ -44,6 +47,7 @@ function App() {
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
<Route path="/login" element={<Login />} />
<Route path="/verify-email" element={<VerifyEmail />} />
<Route path="/cart" element={<Cart />} />
<Route path="/profile" element={<Profile />} />
<Route path="/orders" element={<OrderHistory />} />

View File

@@ -0,0 +1,640 @@
import React, { useState, useEffect, useCallback } from "react";
import axios from "axios";
import {
Upload,
Image as ImageIcon,
FileText,
Video,
File,
Trash2,
Copy,
Check,
Search,
Grid,
List,
Download,
Edit2,
X,
Loader2,
Filter,
} from "lucide-react";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import { Label } from "./ui/label";
import { Textarea } from "./ui/textarea";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "./ui/dialog";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "./ui/alert-dialog";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "./ui/select";
import { toast } from "sonner";
import { useAuth } from "../context/AuthContext";
const API = `${process.env.REACT_APP_BACKEND_URL}/api`;
const MediaManager = ({ onSelect, selectable = false }) => {
const { token } = useAuth();
const [media, setMedia] = useState([]);
const [loading, setLoading] = useState(true);
const [uploading, setUploading] = useState(false);
const [page, setPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [total, setTotal] = useState(0);
const [search, setSearch] = useState("");
const [mediaType, setMediaType] = useState("all");
const [viewMode, setViewMode] = useState("grid");
const [selectedMedia, setSelectedMedia] = useState(null);
const [editDialog, setEditDialog] = useState(false);
const [deleteDialog, setDeleteDialog] = useState(false);
const [mediaToDelete, setMediaToDelete] = useState(null);
const [editForm, setEditForm] = useState({
alt_text: "",
title: "",
description: "",
});
const [copiedId, setCopiedId] = useState(null);
const [dragActive, setDragActive] = useState(false);
const fetchMedia = useCallback(async () => {
try {
setLoading(true);
const params = new URLSearchParams({
page: page.toString(),
limit: "24",
});
if (search) params.append("search", search);
if (mediaType !== "all") params.append("media_type", mediaType);
const response = await axios.get(`${API}/media?${params}`, {
headers: { Authorization: `Bearer ${token}` },
});
setMedia(response.data.items);
setTotalPages(response.data.pages);
setTotal(response.data.total);
} catch (error) {
console.error("Error fetching media:", error);
toast.error("Failed to load media");
} finally {
setLoading(false);
}
}, [token, page, search, mediaType]);
useEffect(() => {
fetchMedia();
}, [fetchMedia]);
const handleUpload = async (files) => {
if (!files || files.length === 0) return;
setUploading(true);
const formData = new FormData();
if (files.length === 1) {
formData.append("file", files[0]);
try {
await axios.post(`${API}/media/upload`, formData, {
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "multipart/form-data",
},
});
toast.success("File uploaded successfully");
fetchMedia();
} catch (error) {
console.error("Upload error:", error);
toast.error(error.response?.data?.detail || "Upload failed");
}
} else {
// Multiple files
for (const file of files) {
formData.append("files", file);
}
try {
const response = await axios.post(
`${API}/media/upload-multiple`,
formData,
{
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "multipart/form-data",
},
},
);
toast.success(`${response.data.total_uploaded} files uploaded`);
if (response.data.total_errors > 0) {
toast.warning(`${response.data.total_errors} files failed to upload`);
}
fetchMedia();
} catch (error) {
console.error("Upload error:", error);
toast.error("Upload failed");
}
}
setUploading(false);
};
const handleDrag = (e) => {
e.preventDefault();
e.stopPropagation();
if (e.type === "dragenter" || e.type === "dragover") {
setDragActive(true);
} else if (e.type === "dragleave") {
setDragActive(false);
}
};
const handleDrop = (e) => {
e.preventDefault();
e.stopPropagation();
setDragActive(false);
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
handleUpload(Array.from(e.dataTransfer.files));
}
};
const handleFileSelect = (e) => {
if (e.target.files && e.target.files.length > 0) {
handleUpload(Array.from(e.target.files));
}
};
const copyUrl = (url) => {
const fullUrl = `${process.env.REACT_APP_BACKEND_URL}${url}`;
navigator.clipboard.writeText(fullUrl);
setCopiedId(url);
toast.success("URL copied to clipboard");
setTimeout(() => setCopiedId(null), 2000);
};
const openEditDialog = (item) => {
setSelectedMedia(item);
setEditForm({
alt_text: item.alt_text || "",
title: item.title || "",
description: item.description || "",
});
setEditDialog(true);
};
const handleUpdate = async () => {
try {
const formData = new FormData();
formData.append("alt_text", editForm.alt_text);
formData.append("title", editForm.title);
formData.append("description", editForm.description);
await axios.put(`${API}/media/${selectedMedia.id}`, formData, {
headers: { Authorization: `Bearer ${token}` },
});
toast.success("Media updated");
setEditDialog(false);
fetchMedia();
} catch (error) {
toast.error("Failed to update media");
}
};
const confirmDelete = (item) => {
setMediaToDelete(item);
setDeleteDialog(true);
};
const handleDelete = async () => {
try {
await axios.delete(`${API}/media/${mediaToDelete.id}`, {
headers: { Authorization: `Bearer ${token}` },
});
toast.success("Media deleted");
setDeleteDialog(false);
setMediaToDelete(null);
fetchMedia();
} catch (error) {
toast.error("Failed to delete media");
}
};
const getMediaIcon = (type) => {
switch (type) {
case "image":
return <ImageIcon className="h-8 w-8" />;
case "video":
return <Video className="h-8 w-8" />;
case "document":
return <FileText className="h-8 w-8" />;
default:
return <File className="h-8 w-8" />;
}
};
const formatFileSize = (bytes) => {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
};
const handleMediaSelect = (item) => {
if (selectable && onSelect) {
onSelect(item);
} else {
setSelectedMedia(item);
}
};
return (
<div className="space-y-6">
{/* Header */}
<div className="flex flex-col md:flex-row gap-4 items-start md:items-center justify-between">
<div>
<h2 className="text-2xl font-bold font-['Outfit']">Media Library</h2>
<p className="text-muted-foreground">
{total} files Upload and manage your images and documents
</p>
</div>
<div className="flex gap-2">
<Button
variant={viewMode === "grid" ? "default" : "outline"}
size="icon"
onClick={() => setViewMode("grid")}
>
<Grid className="h-4 w-4" />
</Button>
<Button
variant={viewMode === "list" ? "default" : "outline"}
size="icon"
onClick={() => setViewMode("list")}
>
<List className="h-4 w-4" />
</Button>
</div>
</div>
{/* Upload Area */}
<div
className={`border-2 border-dashed rounded-xl p-8 text-center transition-colors ${
dragActive
? "border-primary bg-primary/5"
: "border-border hover:border-primary/50"
}`}
onDragEnter={handleDrag}
onDragLeave={handleDrag}
onDragOver={handleDrag}
onDrop={handleDrop}
>
{uploading ? (
<div className="flex flex-col items-center gap-2">
<Loader2 className="h-10 w-10 animate-spin text-primary" />
<p className="text-muted-foreground">Uploading...</p>
</div>
) : (
<>
<Upload className="h-10 w-10 mx-auto text-muted-foreground mb-4" />
<p className="text-lg font-medium mb-2">Drag and drop files here</p>
<p className="text-sm text-muted-foreground mb-4">
Supports: JPG, PNG, GIF, WebP, SVG, PDF, DOC, MP4 and more
</p>
<label htmlFor="file-upload">
<Button asChild>
<span>
<Upload className="h-4 w-4 mr-2" />
Choose Files
</span>
</Button>
<input
id="file-upload"
type="file"
multiple
accept="image/*,.pdf,.doc,.docx,.xls,.xlsx,.mp4,.webm,.mov"
className="hidden"
onChange={handleFileSelect}
/>
</label>
</>
)}
</div>
{/* Filters */}
<div className="flex flex-col md:flex-row gap-4">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search files..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="pl-10"
/>
</div>
<Select value={mediaType} onValueChange={setMediaType}>
<SelectTrigger className="w-full md:w-48">
<Filter className="h-4 w-4 mr-2" />
<SelectValue placeholder="Filter by type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Types</SelectItem>
<SelectItem value="image">Images</SelectItem>
<SelectItem value="document">Documents</SelectItem>
<SelectItem value="video">Videos</SelectItem>
</SelectContent>
</Select>
</div>
{/* Media Grid/List */}
{loading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>
) : media.length === 0 ? (
<div className="text-center py-12">
<ImageIcon className="h-12 w-12 mx-auto text-muted-foreground mb-4" />
<p className="text-lg font-medium">No media files yet</p>
<p className="text-muted-foreground">
Upload your first file to get started
</p>
</div>
) : viewMode === "grid" ? (
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
{media.map((item) => (
<div
key={item.id}
className={`group relative bg-card border border-border rounded-lg overflow-hidden cursor-pointer hover:border-primary transition-colors ${
selectable ? "hover:ring-2 ring-primary" : ""
}`}
onClick={() => handleMediaSelect(item)}
>
<div className="aspect-square bg-muted flex items-center justify-center">
{item.media_type === "image" ? (
<img
src={`${process.env.REACT_APP_BACKEND_URL}${item.file_url}`}
alt={item.alt_text || item.original_filename}
className="w-full h-full object-cover"
/>
) : (
<div className="text-muted-foreground">
{getMediaIcon(item.media_type)}
</div>
)}
</div>
<div className="p-2">
<p className="text-xs truncate font-medium">
{item.original_filename}
</p>
<p className="text-xs text-muted-foreground">
{formatFileSize(item.file_size)}
</p>
</div>
{/* Actions overlay */}
<div className="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2">
<Button
size="icon"
variant="secondary"
onClick={(e) => {
e.stopPropagation();
copyUrl(item.file_url);
}}
>
{copiedId === item.file_url ? (
<Check className="h-4 w-4" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
<Button
size="icon"
variant="secondary"
onClick={(e) => {
e.stopPropagation();
openEditDialog(item);
}}
>
<Edit2 className="h-4 w-4" />
</Button>
<Button
size="icon"
variant="destructive"
onClick={(e) => {
e.stopPropagation();
confirmDelete(item);
}}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
))}
</div>
) : (
<div className="space-y-2">
{media.map((item) => (
<div
key={item.id}
className="flex items-center gap-4 p-4 bg-card border border-border rounded-lg hover:border-primary cursor-pointer transition-colors"
onClick={() => handleMediaSelect(item)}
>
<div className="w-16 h-16 bg-muted rounded flex items-center justify-center flex-shrink-0">
{item.media_type === "image" ? (
<img
src={`${process.env.REACT_APP_BACKEND_URL}${item.file_url}`}
alt={item.alt_text}
className="w-full h-full object-cover rounded"
/>
) : (
getMediaIcon(item.media_type)
)}
</div>
<div className="flex-1 min-w-0">
<p className="font-medium truncate">{item.original_filename}</p>
<p className="text-sm text-muted-foreground">
{formatFileSize(item.file_size)} {item.mime_type}
{item.width &&
item.height &&
`${item.width}×${item.height}`}
</p>
</div>
<div className="flex gap-2">
<Button
size="icon"
variant="outline"
onClick={(e) => {
e.stopPropagation();
copyUrl(item.file_url);
}}
>
{copiedId === item.file_url ? (
<Check className="h-4 w-4" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
<Button
size="icon"
variant="outline"
onClick={(e) => {
e.stopPropagation();
openEditDialog(item);
}}
>
<Edit2 className="h-4 w-4" />
</Button>
<Button
size="icon"
variant="destructive"
onClick={(e) => {
e.stopPropagation();
confirmDelete(item);
}}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
))}
</div>
)}
{/* Pagination */}
{totalPages > 1 && (
<div className="flex items-center justify-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setPage(Math.max(1, page - 1))}
disabled={page === 1}
>
Previous
</Button>
<span className="text-sm text-muted-foreground">
Page {page} of {totalPages}
</span>
<Button
variant="outline"
size="sm"
onClick={() => setPage(Math.min(totalPages, page + 1))}
disabled={page === totalPages}
>
Next
</Button>
</div>
)}
{/* Edit Dialog */}
<Dialog open={editDialog} onOpenChange={setEditDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit Media</DialogTitle>
</DialogHeader>
<div className="space-y-4">
{selectedMedia?.media_type === "image" && (
<div className="aspect-video bg-muted rounded-lg overflow-hidden">
<img
src={`${process.env.REACT_APP_BACKEND_URL}${selectedMedia?.file_url}`}
alt={selectedMedia?.alt_text}
className="w-full h-full object-contain"
/>
</div>
)}
<div className="space-y-2">
<Label>Title</Label>
<Input
value={editForm.title}
onChange={(e) =>
setEditForm({ ...editForm, title: e.target.value })
}
/>
</div>
<div className="space-y-2">
<Label>Alt Text</Label>
<Input
value={editForm.alt_text}
onChange={(e) =>
setEditForm({ ...editForm, alt_text: e.target.value })
}
/>
</div>
<div className="space-y-2">
<Label>Description</Label>
<Textarea
value={editForm.description}
onChange={(e) =>
setEditForm({ ...editForm, description: e.target.value })
}
rows={3}
/>
</div>
<div className="space-y-2">
<Label>URL</Label>
<div className="flex gap-2">
<Input
value={`${process.env.REACT_APP_BACKEND_URL}${selectedMedia?.file_url}`}
readOnly
className="flex-1"
/>
<Button
variant="outline"
onClick={() => copyUrl(selectedMedia?.file_url)}
>
<Copy className="h-4 w-4" />
</Button>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setEditDialog(false)}>
Cancel
</Button>
<Button onClick={handleUpdate}>Save Changes</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Delete Confirmation */}
<AlertDialog open={deleteDialog} onOpenChange={setDeleteDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Media?</AlertDialogTitle>
<AlertDialogDescription>
This will permanently delete "{mediaToDelete?.original_filename}".
This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={handleDelete}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
};
export default MediaManager;

View File

@@ -1,4 +1,4 @@
import React from "react";
import React, { useEffect } from "react";
import { useEditor, EditorContent } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import Placeholder from "@tiptap/extension-placeholder";
@@ -95,10 +95,14 @@ const MenuBar = ({ editor }) => {
};
const RichTextEditor = ({
value,
content,
onChange,
placeholder = "Enter description...",
}) => {
// Support both 'value' and 'content' props for flexibility
const initialContent = value || content || "";
const editor = useEditor({
extensions: [
StarterKit,
@@ -106,12 +110,23 @@ const RichTextEditor = ({
placeholder,
}),
],
content,
content: initialContent,
onUpdate: ({ editor }) => {
onChange(editor.getHTML());
},
});
// Update editor content when value/content prop changes externally
useEffect(() => {
if (editor && initialContent !== undefined) {
const currentContent = editor.getHTML();
// Only update if the content is actually different (prevents cursor jump)
if (currentContent !== initialContent && initialContent !== "<p></p>") {
editor.commands.setContent(initialContent);
}
}
}, [editor, initialContent]);
return (
<div
className="border border-border rounded-md overflow-hidden resize-y min-h-[240px] max-h-[600px]"

View File

@@ -0,0 +1,18 @@
import { useEffect } from "react";
import { useLocation } from "react-router-dom";
/**
* ScrollToTop component that scrolls to the top of the page
* whenever the route changes
*/
function ScrollToTop() {
const { pathname } = useLocation();
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
return null;
}
export default ScrollToTop;

View File

@@ -7,7 +7,7 @@ import { useCart } from "../../context/CartContext";
import { useAuth } from "../../context/AuthContext";
import { toast } from "sonner";
const ProductCard = ({ product }) => {
const ProductCard = ({ product, viewMode = "grid" }) => {
const { addToCart } = useCart();
const { isAuthenticated } = useAuth();
@@ -41,6 +41,96 @@ const ProductCard = ({ product }) => {
return product.image_url;
};
// List View Layout
if (viewMode === "list") {
return (
<Link
to={`/products/${product.id}`}
className="group flex gap-4 overflow-hidden rounded-xl border border-border/50 bg-card hover:border-primary/50 transition-all duration-300 hover-lift card-hover-border p-4"
data-testid={`product-card-${product.id}`}
>
{/* Image Container - Fixed Size */}
<div className="relative w-32 h-32 flex-shrink-0 overflow-hidden bg-muted rounded-lg">
<img
src={getImageUrl()}
alt={product.name}
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
loading="lazy"
/>
{/* Stock Badge */}
{product.stock <= 5 && product.stock > 0 && (
<Badge className="absolute top-2 left-2 bg-orange-500 hover:bg-orange-600 text-xs">
{product.stock} left
</Badge>
)}
{product.stock === 0 && (
<Badge variant="destructive" className="absolute top-2 left-2 text-xs">
Out of Stock
</Badge>
)}
</div>
{/* Content - Flex Container */}
<div className="flex-1 flex flex-col justify-between min-w-0">
<div className="space-y-2">
{/* Category & Brand */}
<div className="flex items-center gap-2">
<Badge variant="secondary" className="text-xs capitalize">
{product.category}
</Badge>
{product.brand && (
<span className="text-xs text-muted-foreground">
{product.brand}
</span>
)}
</div>
{/* Title */}
<h3 className="font-semibold text-base leading-tight line-clamp-1 group-hover:text-primary transition-colors font-['Outfit']">
{product.name}
</h3>
{/* Description if available */}
{product.description && (
<p className="text-sm text-muted-foreground line-clamp-2">
{product.description}
</p>
)}
</div>
{/* Price & Actions */}
<div className="flex items-center justify-between pt-2">
<span className="text-xl font-bold font-['Outfit']">
${product.price.toFixed(2)}
</span>
<div className="flex items-center gap-2">
<Button
size="sm"
variant="outline"
className="rounded-full"
data-testid={`view-product-${product.id}`}
>
<Eye className="h-4 w-4 mr-1" />
View
</Button>
<Button
size="sm"
className="rounded-full px-4 btn-press"
onClick={handleAddToCart}
disabled={product.stock === 0}
data-testid={`add-to-cart-${product.id}`}
>
<ShoppingCart className="h-4 w-4 mr-1" />
Add to Cart
</Button>
</div>
</div>
</div>
</Link>
);
}
// Grid View Layout (Default)
return (
<Link
to={`/products/${product.id}`}

View File

@@ -49,9 +49,10 @@ const ServiceCard = ({ service }) => {
{service.name}
</h3>
<p className="text-sm text-muted-foreground line-clamp-2">
{service.description}
</p>
<div
className="text-sm text-muted-foreground line-clamp-2 prose prose-sm max-w-none"
dangerouslySetInnerHTML={{ __html: service.description }}
/>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Clock className="h-4 w-4" />
@@ -60,9 +61,13 @@ const ServiceCard = ({ service }) => {
<div className="flex items-center justify-between pt-3 border-t border-border">
<div>
<span className="text-xs text-muted-foreground">Starting from</span>
<span className="text-xs text-muted-foreground">
{service.price > 0 ? "Starting from" : "Price"}
</span>
<p className="text-xl font-bold font-['Outfit']">
${service.price.toFixed(2)}
{service.price > 0
? `$${service.price.toFixed(2)}`
: "Contact for quote"}
</p>
</div>
<Button

View File

@@ -168,25 +168,25 @@ const Footer = () => {
<li className="flex items-start gap-3">
<MapPin className="h-4 w-4 mt-0.5 text-muted-foreground flex-shrink-0" />
<span className="text-sm text-muted-foreground">
123 Tech Street, Silicon Valley, CA 94000
Belmopan City, Belize
</span>
</li>
<li className="flex items-center gap-3">
<Phone className="h-4 w-4 text-muted-foreground flex-shrink-0" />
<a
href="tel:+1234567890"
href="tel:+5016386318"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
+1 (234) 567-890
(501) 638-6318
</a>
</li>
<li className="flex items-center gap-3">
<Mail className="h-4 w-4 text-muted-foreground flex-shrink-0" />
<a
href="mailto:info@prompttechsolutions.com"
href="mailto:prompttechbz@gmail.com"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
info@prompttechsolutions.com
prompttechbz@gmail.com
</a>
</li>
</ul>

View File

@@ -45,12 +45,12 @@ const Navbar = () => {
return (
<header className="sticky top-0 z-50 glass glass-border">
<nav className="max-w-7xl mx-auto px-4 md:px-8">
<nav className="max-w-7xl mx-auto px-4 md:px-8 relative">
<div className="flex items-center justify-between h-16">
{/* Logo */}
<Link
to="/"
className="flex items-center gap-2 group"
className="flex items-center gap-2 group flex-shrink-0"
data-testid="navbar-logo"
>
<img
@@ -63,8 +63,8 @@ const Navbar = () => {
</span>
</Link>
{/* Desktop Navigation */}
<div className="hidden md:flex items-center gap-1">
{/* Desktop Navigation - Centered */}
<div className="hidden md:flex items-center gap-1 absolute left-1/2 transform -translate-x-1/2">
{navLinks.map((link) => (
<Link
key={link.path}

View File

@@ -1,63 +1,186 @@
import React, { useEffect } from "react";
import React, { useEffect, useState, useRef } from "react";
import { Link } from "react-router-dom";
import { Users, Target, Award, Heart, ArrowRight } from "lucide-react";
import {
Users,
Target,
Award,
Heart,
ArrowRight,
Zap,
Shield,
Star,
Lightbulb,
TrendingUp,
UserCheck,
Sparkles,
} from "lucide-react";
import { Button } from "../components/ui/button";
import { Badge } from "../components/ui/badge";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
} from "../components/ui/dialog";
import axios from "axios";
const API = `${process.env.REACT_APP_BACKEND_URL}/api`;
// Icon mapping for company values
const iconMap = {
Target: Target,
Users: Users,
Award: Award,
Heart: Heart,
Zap: Zap,
Shield: Shield,
Star: Star,
Lightbulb: Lightbulb,
TrendingUp: TrendingUp,
UserCheck: UserCheck,
Sparkles: Sparkles,
};
// Default icons for common values
const getDefaultIcon = (title) => {
const lowerTitle = title.toLowerCase();
if (lowerTitle.includes("quality")) return Sparkles;
if (lowerTitle.includes("customer") || lowerTitle.includes("focus"))
return UserCheck;
if (lowerTitle.includes("excellence") || lowerTitle.includes("excellent"))
return TrendingUp;
if (lowerTitle.includes("integrity") || lowerTitle.includes("honest"))
return Shield;
return Target;
};
const About = () => {
const [team, setTeam] = useState([]);
const [values, setValues] = useState([]);
const [content, setContent] = useState({});
const [loading, setLoading] = useState(true);
const [valuesHeadingVisible, setValuesHeadingVisible] = useState(false);
const [visibleCards, setVisibleCards] = useState([]);
const [selectedValue, setSelectedValue] = useState(null);
const [dialogOpen, setDialogOpen] = useState(false);
const valuesHeadingRef = useRef(null);
const valueCardsRefs = useRef([]);
useEffect(() => {
window.scrollTo(0, 0);
fetchAboutData();
}, []);
const team = [
{
name: "Alex Johnson",
role: "Founder & CEO",
image:
"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400",
},
{
name: "Sarah Williams",
role: "Head of Operations",
image:
"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=400",
},
{
name: "Mike Chen",
role: "Lead Technician",
image:
"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=400",
},
{
name: "Emily Davis",
role: "Customer Success",
image:
"https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=400",
},
];
useEffect(() => {
// Observer for heading
const headingObserver = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setValuesHeadingVisible(true);
}
});
},
{ threshold: 0.3 },
);
const values = [
{
icon: Target,
title: "Quality First",
desc: "We never compromise on the quality of our products and services.",
},
{
icon: Users,
title: "Customer Focus",
desc: "Your satisfaction is our top priority. We listen and deliver.",
},
{
icon: Award,
title: "Excellence",
desc: "We strive for excellence in everything we do.",
},
{
icon: Heart,
title: "Integrity",
desc: "Honest, transparent, and ethical business practices.",
},
];
if (valuesHeadingRef.current) {
headingObserver.observe(valuesHeadingRef.current);
}
// Observer for individual cards
const cardsObserver = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const index = parseInt(entry.target.dataset.index);
setVisibleCards((prev) => [...new Set([...prev, index])]);
}
});
},
{ threshold: 0.3 },
);
valueCardsRefs.current.forEach((ref) => {
if (ref) cardsObserver.observe(ref);
});
return () => {
if (valuesHeadingRef.current) {
headingObserver.unobserve(valuesHeadingRef.current);
}
valueCardsRefs.current.forEach((ref) => {
if (ref) cardsObserver.unobserve(ref);
});
};
}, [values]);
const fetchAboutData = async () => {
try {
const [teamRes, valuesRes, contentRes] = await Promise.all([
axios.get(`${API}/about/team`),
axios.get(`${API}/about/values`),
axios.get(`${API}/about/content`),
]);
setTeam(teamRes.data || []);
setValues(valuesRes.data || []);
// Convert content array to object keyed by section
const contentObj = {};
(contentRes.data || []).forEach((item) => {
contentObj[item.section] = item;
});
setContent(contentObj);
} catch (error) {
console.error("Error fetching about data:", error);
// Use fallback data if API fails
setTeam([]);
setValues([
{
icon: "Target",
title: "Quality First",
description:
"We never compromise on the quality of our products and services.",
},
{
icon: "Users",
title: "Customer Focus",
description:
"Your satisfaction is our top priority. We listen and deliver.",
},
{
icon: "Award",
title: "Excellence",
description: "We strive for excellence in everything we do.",
},
{
icon: "Heart",
title: "Integrity",
description: "Honest, transparent, and ethical business practices.",
},
]);
} finally {
setLoading(false);
}
};
// Get icon component from string name
const getIcon = (iconName, title) => {
if (iconName && iconMap[iconName]) {
return iconMap[iconName];
}
return getDefaultIcon(title);
};
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="animate-spin w-8 h-8 border-2 border-primary border-t-transparent rounded-full" />
</div>
);
}
return (
<div className="min-h-screen">
@@ -70,15 +193,15 @@ const About = () => {
About PromptTech Solutions
</Badge>
<h1 className="text-4xl sm:text-5xl font-bold font-['Outfit'] leading-tight">
Your Trusted
{content.hero?.title || "Your Trusted"}
<br />
<span className="text-muted-foreground">Tech Partner</span>
<span className="text-muted-foreground">
{content.hero?.subtitle || "Tech Partner"}
</span>
</h1>
<p className="text-lg text-muted-foreground leading-relaxed">
Founded in 2020, PromptTech Solutions has grown from a small
repair shop to a comprehensive tech solutions provider. We
combine quality products with expert services to deliver the
best tech experience.
{content.hero?.content ||
"Founded in 2021, PromptTech Solutions has evolved from a small repair shop into a comprehensive tech solutions provider. We're here to guide you through any technology challenge—whether it's laptops, desktops, smartphones, or other devices. With expert service and personalized support, we deliver reliable solutions for all your tech needs."}
</p>
<div className="flex flex-wrap gap-4">
<Link to="/products" data-testid="about-shop-now">
@@ -97,9 +220,15 @@ const About = () => {
<div className="relative">
<img
src="https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=800"
alt="Our Team"
className="rounded-2xl shadow-2xl"
src={
content.hero?.image_url
? content.hero.image_url.startsWith('/uploads')
? `${process.env.REACT_APP_BACKEND_URL}${content.hero.image_url}`
: content.hero.image_url
: `${process.env.REACT_APP_BACKEND_URL}/uploads/media/aa5bcc15-3b1e-4ed8-8708-1a3dceb9494d.jpg`
}
alt="Tech Repair Services"
className="rounded-2xl shadow-2xl w-full h-auto max-h-[500px] object-cover"
data-testid="about-hero-image"
/>
{/* Stats Card */}
@@ -115,26 +244,108 @@ const About = () => {
</section>
{/* Stats */}
<section className="py-12 bg-muted/30">
<section className="py-12 bg-muted/30 overflow-hidden">
<div className="max-w-7xl mx-auto px-4 md:px-8">
<div className="grid grid-cols-2 md:grid-cols-4 gap-8">
{[
{ value: "50K+", label: "Happy Customers" },
{ value: "10K+", label: "Products Sold" },
{ value: "15K+", label: "Repairs Done" },
{ value: "98%", label: "Satisfaction Rate" },
].map((stat, idx) => (
<div
key={idx}
className="text-center"
data-testid={`stat-${idx}`}
>
<p className="text-4xl md:text-5xl font-bold font-['Outfit'] mb-2">
{stat.value}
</p>
<p className="text-muted-foreground">{stat.label}</p>
<div className="relative">
<style>{`
@keyframes scroll-left {
0% { transform: translateX(0); }
100% { transform: translateX(-100%); }
}
.animate-scroll {
animation: scroll-left 15s linear infinite;
}
`}</style>
<div className="flex">
<div className="flex animate-scroll">
{/* First set */}
{(
content.stats?.data?.stats || [
{ value: "1K+", label: "Happy Customers" },
{ value: "500+", label: "Products Sold" },
{ value: "1,500+", label: "Repairs Done" },
{ value: "90%", label: "Satisfaction Rate" },
]
).map((stat, idx) => (
<div
key={idx}
className="text-center flex-shrink-0 px-8 md:px-16 min-w-[200px]"
data-testid={`stat-${idx}`}
>
<p className="text-4xl md:text-5xl font-bold font-['Outfit'] mb-2">
{stat.value}
</p>
<p className="text-muted-foreground whitespace-nowrap">
{stat.label}
</p>
</div>
))}
{/* Duplicate set for seamless loop */}
{(
content.stats?.data?.stats || [
{ value: "1K+", label: "Happy Customers" },
{ value: "500+", label: "Products Sold" },
{ value: "1,500+", label: "Repairs Done" },
{ value: "90%", label: "Satisfaction Rate" },
]
).map((stat, idx) => (
<div
key={`dup-${idx}`}
className="text-center flex-shrink-0 px-8 md:px-16 min-w-[200px]"
>
<p className="text-4xl md:text-5xl font-bold font-['Outfit'] mb-2">
{stat.value}
</p>
<p className="text-muted-foreground whitespace-nowrap">
{stat.label}
</p>
</div>
))}
</div>
))}
{/* Duplicate entire animation block for truly seamless loop */}
<div className="flex animate-scroll" aria-hidden="true">
{(
content.stats?.data?.stats || [
{ value: "1K+", label: "Happy Customers" },
{ value: "500+", label: "Products Sold" },
{ value: "1,500+", label: "Repairs Done" },
{ value: "90%", label: "Satisfaction Rate" },
]
).map((stat, idx) => (
<div
key={`set2-${idx}`}
className="text-center flex-shrink-0 px-8 md:px-16 min-w-[200px]"
>
<p className="text-4xl md:text-5xl font-bold font-['Outfit'] mb-2">
{stat.value}
</p>
<p className="text-muted-foreground whitespace-nowrap">
{stat.label}
</p>
</div>
))}
{(
content.stats?.data?.stats || [
{ value: "1K+", label: "Happy Customers" },
{ value: "500+", label: "Products Sold" },
{ value: "1,500+", label: "Repairs Done" },
{ value: "90%", label: "Satisfaction Rate" },
]
).map((stat, idx) => (
<div
key={`set2-dup-${idx}`}
className="text-center flex-shrink-0 px-8 md:px-16 min-w-[200px]"
>
<p className="text-4xl md:text-5xl font-bold font-['Outfit'] mb-2">
{stat.value}
</p>
<p className="text-muted-foreground whitespace-nowrap">
{stat.label}
</p>
</div>
))}
</div>
</div>
</div>
</div>
</section>
@@ -144,28 +355,37 @@ const About = () => {
<div className="max-w-4xl mx-auto px-4 md:px-8">
<div className="text-center mb-12">
<h2 className="text-3xl md:text-4xl font-bold font-['Outfit'] mb-4">
Our Story
{content.story?.title || "Our Story"}
</h2>
</div>
<div className="prose prose-lg dark:prose-invert max-w-none">
<p className="text-muted-foreground leading-relaxed mb-6">
PromptTech Solutions started with a simple vision: to make quality
tech accessible and provide expert support that customers can
trust. What began as a small phone repair shop has evolved into a
full-service tech destination.
</p>
<p className="text-muted-foreground leading-relaxed mb-6">
Our team of certified technicians brings decades of combined
experience in electronics repair, from smartphones to laptops and
everything in between. We've helped thousands of customers bring
their devices back to life.
</p>
<p className="text-muted-foreground leading-relaxed">
Today, we're proud to offer a curated selection of premium
electronics alongside our repair services. Every product we sell
meets our high standards for quality, and every repair we do is
backed by our satisfaction guarantee.
</p>
{content.story?.content ? (
<div
className="text-muted-foreground leading-relaxed"
dangerouslySetInnerHTML={{ __html: content.story.content }}
/>
) : (
<>
<p className="text-muted-foreground leading-relaxed mb-6">
PromptTech Solutions started with a simple vision: to make
quality tech accessible and provide expert support that
customers can trust. What began as a small phone repair shop
has evolved into a full-service tech destination.
</p>
<p className="text-muted-foreground leading-relaxed mb-6">
Our team of certified technicians brings decades of combined
experience in electronics repair, from smartphones to laptops
and everything in between. We've helped thousands of customers
bring their devices back to life.
</p>
<p className="text-muted-foreground leading-relaxed">
Today, we're proud to offer a curated selection of premium
electronics alongside our repair services. Every product we
sell meets our high standards for quality, and every repair we
do is backed by our satisfaction guarantee.
</p>
</>
)}
</div>
</div>
</section>
@@ -173,7 +393,10 @@ const About = () => {
{/* Values */}
<section className="py-16 md:py-24 bg-muted/30">
<div className="max-w-7xl mx-auto px-4 md:px-8">
<div className="text-center mb-12">
<div
ref={valuesHeadingRef}
className={`text-center mb-12 transition-all duration-700 ${valuesHeadingVisible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-10"}`}
>
<h2 className="text-3xl md:text-4xl font-bold font-['Outfit'] mb-4">
Our Values
</h2>
@@ -183,61 +406,226 @@ const About = () => {
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{values.map((value, idx) => {
const Icon = value.icon;
return (
<div
key={idx}
className="p-6 rounded-2xl bg-card border border-border hover-lift text-center"
data-testid={`value-${idx}`}
>
<div className="w-14 h-14 mx-auto mb-4 rounded-full bg-primary/10 flex items-center justify-center">
<Icon className="h-7 w-7 text-primary" />
</div>
<h3 className="text-lg font-semibold mb-2 font-['Outfit']">
{value.title}
</h3>
<p className="text-sm text-muted-foreground">{value.desc}</p>
</div>
);
})}
<style>{`
@keyframes slide-up-fade {
from {
opacity: 0;
transform: translateY(60px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.value-card {
opacity: 0;
transform: translateY(60px);
cursor: pointer;
transition: all 0.3s ease;
}
.value-card.visible {
animation: slide-up-fade 0.8s ease-out forwards;
}
.value-card:hover {
transform: translateY(-8px) scale(1.02);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
}
.value-card:hover .card-bg {
opacity: 1;
}
.card-bg {
position: absolute;
inset: 0;
border-radius: 1rem;
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
}
.quality-bg { background: linear-gradient(135deg, rgba(99, 102, 241, 0.1), rgba(168, 85, 247, 0.1)); }
.customer-bg { background: linear-gradient(135deg, rgba(59, 130, 246, 0.1), rgba(14, 165, 233, 0.1)); }
.excellence-bg { background: linear-gradient(135deg, rgba(239, 68, 68, 0.1), rgba(249, 115, 22, 0.1)); }
.integrity-bg { background: linear-gradient(135deg, rgba(34, 197, 94, 0.1), rgba(16, 185, 129, 0.1)); }
`}</style>
{values.length > 0
? values.map((value, idx) => {
const Icon = getIcon(value.icon, value.title);
const bgClass = [
"quality-bg",
"customer-bg",
"excellence-bg",
"integrity-bg",
][idx % 4];
return (
<div
key={value.id || idx}
ref={(el) => (valueCardsRefs.current[idx] = el)}
data-index={idx}
className={`value-card relative p-6 rounded-2xl bg-card border border-border text-center overflow-hidden ${visibleCards.includes(idx) ? "visible" : ""}`}
onClick={() => {
setSelectedValue(value);
setDialogOpen(true);
}}
data-testid={`value-${idx}`}
>
<div className={`card-bg ${bgClass}`} />
<div className="relative z-10">
<div className="w-14 h-14 mx-auto mb-4 rounded-full bg-primary/10 flex items-center justify-center">
<Icon className="h-7 w-7 text-primary" />
</div>
<h3 className="text-lg font-semibold mb-2 font-['Outfit']">
{value.title}
</h3>
<p className="text-sm text-muted-foreground">
{value.description}
</p>
</div>
</div>
);
})
: // Fallback values
[
{
icon: Sparkles,
title: "Quality First",
desc: "We never compromise on the quality of our products and services.",
},
{
icon: UserCheck,
title: "Customer Focus",
desc: "Your satisfaction is our top priority. We listen and deliver.",
},
{
icon: TrendingUp,
title: "Excellence",
desc: "We strive for excellence in everything we do.",
},
{
icon: Shield,
title: "Integrity",
desc: "Honest, transparent, and ethical business practices.",
},
].map((value, idx) => {
const Icon = value.icon;
const bgClass = [
"quality-bg",
"customer-bg",
"excellence-bg",
"integrity-bg",
][idx];
return (
<div
key={idx}
ref={(el) => (valueCardsRefs.current[idx] = el)}
data-index={idx}
className={`value-card relative p-6 rounded-2xl bg-card border border-border text-center overflow-hidden ${visibleCards.includes(idx) ? "visible" : ""}`}
onClick={() => {
setSelectedValue({
title: value.title,
description: value.desc,
icon: value.icon,
});
setDialogOpen(true);
}}
data-testid={`value-${idx}`}
>
<div className={`card-bg ${bgClass}`} />
<div className="relative z-10">
<div className="w-14 h-14 mx-auto mb-4 rounded-full bg-primary/10 flex items-center justify-center">
<Icon className="h-7 w-7 text-primary" />
</div>
<h3 className="text-lg font-semibold mb-2 font-['Outfit']">
{value.title}
</h3>
<p className="text-sm text-muted-foreground">
{value.desc}
</p>
</div>
</div>
);
})}
</div>
</div>
{/* Value Detail Dialog */}
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
{selectedValue && (
<>
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-primary/10 flex items-center justify-center">
{React.createElement(
getIcon(selectedValue.icon, selectedValue.title),
{ className: "h-8 w-8 text-primary" },
)}
</div>
<DialogTitle className="text-2xl font-bold text-center font-['Outfit']">
{selectedValue.title}
</DialogTitle>
<DialogDescription className="text-center text-base mt-4">
{selectedValue.description}
</DialogDescription>
<div className="mt-6 p-6 rounded-lg bg-muted/50">
<p className="text-sm text-muted-foreground leading-relaxed">
At PromptTech Solutions,{" "}
<strong>{selectedValue.title.toLowerCase()}</strong> is at
the core of everything we do. We believe that{" "}
{selectedValue.description.toLowerCase()} This commitment
drives us to deliver exceptional service and build lasting
relationships with our customers.
</p>
</div>
</>
)}
</DialogHeader>
</DialogContent>
</Dialog>
</section>
{/* Team */}
<section className="py-16 md:py-24">
<div className="max-w-7xl mx-auto px-4 md:px-8">
<div className="text-center mb-12">
<h2 className="text-3xl md:text-4xl font-bold font-['Outfit'] mb-4">
Meet Our Team
</h2>
<p className="text-muted-foreground max-w-2xl mx-auto">
The people behind PromptTech Solutions' success
</p>
</div>
{team.length > 0 && (
<section className="py-16 md:py-24">
<div className="max-w-7xl mx-auto px-4 md:px-8">
<div className="text-center mb-12">
<h2 className="text-3xl md:text-4xl font-bold font-['Outfit'] mb-4">
Meet Our Team
</h2>
<p className="text-muted-foreground max-w-2xl mx-auto">
The people behind PromptTech Solutions' success
</p>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
{team.map((member, idx) => (
<div
key={idx}
className="group text-center"
data-testid={`team-member-${idx}`}
>
<div className="relative mb-4 overflow-hidden rounded-2xl aspect-square">
<img
src={member.image}
alt={member.name}
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
/>
<div
className={`grid grid-cols-2 ${team.length <= 3 ? "md:grid-cols-3" : "md:grid-cols-4"} gap-6`}
>
{team.map((member, idx) => (
<div
key={member.id || idx}
className="group text-center"
data-testid={`team-member-${idx}`}
>
<div className="relative mb-4 overflow-hidden rounded-2xl aspect-square">
<img
src={
member.image_url
? member.image_url.startsWith('/uploads')
? `${process.env.REACT_APP_BACKEND_URL}${member.image_url}`
: member.image_url
: "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=400"
}
alt={member.name}
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
/>
</div>
<h3 className="font-semibold font-['Outfit']">
{member.name}
</h3>
<p className="text-sm text-muted-foreground">{member.role}</p>
</div>
<h3 className="font-semibold font-['Outfit']">{member.name}</h3>
<p className="text-sm text-muted-foreground">{member.role}</p>
</div>
))}
))}
</div>
</div>
</div>
</section>
</section>
)}
{/* CTA */}
<section className="py-16 md:py-24 bg-primary text-primary-foreground">

File diff suppressed because it is too large Load Diff

View File

@@ -41,24 +41,24 @@ const Contact = () => {
{
icon: MapPin,
title: "Address",
content: "123 Tech Street, Silicon Valley, CA 94000",
content: "Belmopan City, Belize",
},
{
icon: Phone,
title: "Phone",
content: "+1 (234) 567-890",
link: "tel:+1234567890",
content: "(501) 638-6318",
link: "tel:+5016386318",
},
{
icon: Mail,
title: "Email",
content: "info@prompttechsolutions.com",
link: "mailto:info@prompttechsolutions.com",
content: "prompttechbz@gmail.com",
link: "mailto:prompttechbz@gmail.com",
},
{
icon: Clock,
title: "Business Hours",
content: "Mon - Sat: 9AM - 7PM",
content: "Mon-Fri: 8AM-5PM | Sat: 9AM-5PM",
},
];
@@ -113,14 +113,32 @@ const Contact = () => {
);
})}
{/* Map placeholder */}
<div className="aspect-video rounded-xl overflow-hidden border border-border bg-muted">
<img
src="https://images.unsplash.com/photo-1526778548025-fa2f459cd5c1?w=800"
alt="Location Map"
className="w-full h-full object-cover"
data-testid="contact-map"
{/* Google Map - Belmopan City, Belize */}
<div
className="aspect-video rounded-xl overflow-hidden border border-border bg-muted relative group"
data-testid="contact-map"
>
<iframe
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d30389.40536428!2d-88.7772!3d17.2514!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x8f5d0e6c6c6d4e6f%3A0x9f0b8c9e9e9e9e9e!2sBelmopan%2C%20Belize!5e0!3m2!1sen!2sus!4v1706824800000!5m2!1sen!2sus"
width="100%"
height="100%"
style={{ border: 0 }}
allowFullScreen=""
loading="lazy"
referrerPolicy="no-referrer-when-downgrade"
title="PromptTech Solutions Location - Belmopan City, Belize"
className="w-full h-full"
/>
{/* Ctrl + Scroll overlay */}
<div
className="absolute inset-0 bg-black/50 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none"
style={{ pointerEvents: "none" }}
>
<div className="bg-background/90 px-4 py-2 rounded-lg text-sm font-medium flex items-center gap-2">
<kbd className="px-2 py-1 bg-muted rounded text-xs">Ctrl</kbd>
<span>+ scroll to zoom</span>
</div>
</div>
</div>
</div>
@@ -227,7 +245,7 @@ const Contact = () => {
{[
{
q: "What are your business hours?",
a: "We are open Monday through Saturday, 9AM to 7PM. We are closed on Sundays.",
a: "We are open Monday to Friday, 8AM to 5PM, and Saturday, 9AM to 5PM. We are closed on Sundays.",
},
{
q: "Do you offer warranty on repairs?",
@@ -239,7 +257,7 @@ const Contact = () => {
},
{
q: "Do you offer pickup and delivery?",
a: "Yes, we offer free pickup and delivery for repairs within a 10-mile radius.",
a: "Yes, we do offer pickups within Belmopan and free delivery within Belmopan. Anything outside Belmopan region must be either with a carrier or with BPMS or Interdistrict Belize covered by the customer.",
},
].map((faq, idx) => (
<div

View File

@@ -58,14 +58,21 @@ const Home = () => {
}, []);
const features = [
{ icon: Truck, title: "Free Shipping", desc: "On orders over $100" },
{ icon: Shield, title: "Warranty", desc: "1 Year manufacturer warranty" },
{
icon: Shield,
title: "6 Months Warranty",
desc: "Comprehensive coverage",
},
{
icon: Headphones,
title: "24/7 Support",
desc: "Expert assistance anytime",
title: "Monday-Friday Support",
desc: "Expert assistance 8 AM - 5 PM",
},
{
icon: Wrench,
title: "Certified Technician",
desc: "Professional repair service",
},
{ icon: Wrench, title: "Expert Repair", desc: "Certified technicians" },
];
const categories = [
@@ -86,7 +93,7 @@ const Home = () => {
New Arrivals Available
</Badge>
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-bold leading-tight tracking-tight font-['Outfit']">
Premium Tech,
PromptTech Solution,
<br />
<span className="text-muted-foreground">Expert Service</span>
</h1>
@@ -141,13 +148,13 @@ const Home = () => {
{/* Features Bar */}
<section className="border-y border-border bg-muted/30 py-8">
<div className="max-w-7xl mx-auto px-4 md:px-8">
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{features.map((feature, idx) => {
const Icon = feature.icon;
return (
<div
key={idx}
className="flex items-center gap-3"
className="flex flex-col items-center text-center gap-3"
data-testid={`feature-${idx}`}
>
<div className="w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0">

View File

@@ -1,5 +1,10 @@
import React, { useState } from "react";
import { Link, useNavigate, useLocation } from "react-router-dom";
import React, { useState, useEffect } from "react";
import {
Link,
useNavigate,
useLocation,
useSearchParams,
} from "react-router-dom";
import { Mail, Lock, User, ArrowRight, Eye, EyeOff } from "lucide-react";
import { Button } from "../components/ui/button";
import { Input } from "../components/ui/input";
@@ -8,29 +13,51 @@ import { Separator } from "../components/ui/separator";
import { useAuth } from "../context/AuthContext";
import { toast } from "sonner";
const API = `${process.env.REACT_APP_BACKEND_URL}/api`;
const Login = () => {
const navigate = useNavigate();
const location = useLocation();
const [searchParams] = useSearchParams();
const { login, register } = useAuth();
const [isRegister, setIsRegister] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState({
name: "",
firstName: "",
lastName: "",
email: "",
password: "",
});
const from = location.state?.from?.pathname || "/";
// Handle OAuth callback token
useEffect(() => {
const token = searchParams.get("token");
const error = searchParams.get("error");
if (token) {
// Store token and redirect
localStorage.setItem("token", token);
toast.success("Successfully logged in!");
navigate(from, { replace: true });
} else if (error) {
toast.error("Authentication failed. Please try again.");
}
}, [searchParams, navigate, from]);
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
try {
if (isRegister) {
await register(formData.name, formData.email, formData.password);
toast.success("Account created successfully!");
const fullName = `${formData.firstName} ${formData.lastName}`.trim();
await register(fullName, formData.email, formData.password);
toast.success(
"Account created! Please check your email to verify your account.",
);
} else {
await login(formData.email, formData.password);
toast.success("Welcome back!");
@@ -43,6 +70,11 @@ const Login = () => {
}
};
const handleSocialLogin = (provider) => {
// Redirect to backend OAuth endpoint
window.location.href = `${API}/auth/${provider}`;
};
return (
<div className="min-h-screen flex items-center justify-center py-12 px-4">
<div className="w-full max-w-md">
@@ -74,23 +106,36 @@ const Login = () => {
<div className="border border-border rounded-2xl bg-card p-6 md:p-8">
<form onSubmit={handleSubmit} className="space-y-4">
{isRegister && (
<div className="space-y-2">
<Label htmlFor="name">Full Name</Label>
<div className="relative">
<User className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
id="name"
placeholder="John Doe"
className="pl-10"
value={formData.name}
onChange={(e) =>
setFormData({ ...formData, name: e.target.value })
}
required={isRegister}
data-testid="register-name"
/>
<>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="firstName">First Name</Label>
<Input
id="firstName"
placeholder="John"
value={formData.firstName}
onChange={(e) =>
setFormData({ ...formData, firstName: e.target.value })
}
required={isRegister}
data-testid="register-firstname"
/>
</div>
<div className="space-y-2">
<Label htmlFor="lastName">Last Name</Label>
<Input
id="lastName"
placeholder="Doe"
value={formData.lastName}
onChange={(e) =>
setFormData({ ...formData, lastName: e.target.value })
}
required={isRegister}
data-testid="register-lastname"
/>
</div>
</div>
</div>
</>
)}
<div className="space-y-2">
@@ -166,14 +211,80 @@ const Login = () => {
{loading
? "Please wait..."
: isRegister
? "Create Account"
: "Sign In"}
? "Create Account"
: "Sign In"}
<ArrowRight className="h-4 w-4" />
</Button>
</form>
<Separator className="my-6" />
{/* Social Login Buttons */}
<div className="space-y-3">
<p className="text-center text-sm text-muted-foreground mb-4">
Or continue with
</p>
<Button
type="button"
variant="outline"
className="w-full"
onClick={() => handleSocialLogin("google")}
data-testid="google-login"
>
<svg className="w-5 h-5 mr-2" viewBox="0 0 24 24">
<path
fill="#4285F4"
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
/>
<path
fill="#34A853"
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
/>
<path
fill="#FBBC05"
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
/>
<path
fill="#EA4335"
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
/>
</svg>
Sign in with Google
</Button>
<Button
type="button"
variant="outline"
className="w-full"
onClick={() => handleSocialLogin("facebook")}
data-testid="facebook-login"
>
<svg className="w-5 h-5 mr-2" fill="#1877F2" viewBox="0 0 24 24">
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z" />
</svg>
Sign in with Facebook
</Button>
<Button
type="button"
variant="outline"
className="w-full"
onClick={() => handleSocialLogin("yahoo")}
data-testid="yahoo-login"
>
<svg className="w-5 h-5 mr-2" viewBox="0 0 24 24">
<path
fill="#5F01D1"
d="M13.131 21.415v-6.844l5.943-11.571h-4.161l-3.281 6.865-3.281-6.865H4.19l5.876 11.497v6.918z"
/>
</svg>
Sign in with Yahoo
</Button>
</div>
<Separator className="my-6" />
<p className="text-center text-sm text-muted-foreground">
{isRegister ? "Already have an account?" : "Don't have an account?"}{" "}
<button

View File

@@ -43,14 +43,36 @@ const ProductDetail = () => {
fetchProduct();
}, [id]);
const fetchProduct = async () => {
// Auto-refresh every 5 seconds for real-time updates
useEffect(() => {
const interval = setInterval(() => {
fetchProduct(true);
}, 5000);
return () => clearInterval(interval);
}, [id]);
// Refresh when user returns to the tab
useEffect(() => {
const handleVisibilityChange = () => {
if (!document.hidden) {
fetchProduct();
}
};
document.addEventListener("visibilitychange", handleVisibilityChange);
return () =>
document.removeEventListener("visibilitychange", handleVisibilityChange);
}, [id]);
const fetchProduct = async (silent = false) => {
try {
const response = await axios.get(`${API}/products/${id}`);
setProduct(response.data);
} catch (error) {
console.error("Failed to fetch product:", error);
} finally {
setLoading(false);
if (!silent) setLoading(false);
}
};
@@ -83,7 +105,7 @@ const ProductDetail = () => {
},
{
headers: { Authorization: `Bearer ${token}` },
}
},
);
toast.success("Review submitted successfully!");
setReviewForm({ rating: 5, title: "", comment: "" });

View File

@@ -37,7 +37,7 @@ const Products = () => {
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState(searchParams.get("search") || "");
const [category, setCategory] = useState(
searchParams.get("category") || "all"
searchParams.get("category") || "all",
);
const [priceRange, setPriceRange] = useState([0, 3000]);
const [sortBy, setSortBy] = useState("name");
@@ -62,25 +62,37 @@ const Products = () => {
fetchProducts();
}, [category, search]);
const fetchProducts = async () => {
setLoading(true);
// Auto-refresh every 5 seconds for real-time updates
useEffect(() => {
const interval = setInterval(() => {
fetchProducts(true); // Silent refresh without loading spinner
}, 5000); // 5 seconds
return () => clearInterval(interval);
}, [category, search]);
// Refresh products when user returns to the tab
useEffect(() => {
const handleVisibilityChange = () => {
if (!document.hidden) {
fetchProducts();
}
};
document.addEventListener("visibilitychange", handleVisibilityChange);
return () =>
document.removeEventListener("visibilitychange", handleVisibilityChange);
}, [category, search]);
const fetchProducts = async (silent = false) => {
if (!silent) setLoading(true);
try {
const params = new URLSearchParams();
if (category && category !== "all") params.append("category", category);
if (search) params.append("search", search);
const cacheKey = `products-${params.toString()}`;
const cached = getCached(cacheKey);
if (cached) {
setProducts(cached);
setLoading(false);
return;
}
const response = await axios.get(`${API}/products?${params.toString()}`);
setProducts(response.data);
setCache(cacheKey, response.data);
} catch (error) {
console.error("Failed to fetch products:", error);
} finally {
@@ -376,7 +388,7 @@ const Products = () => {
}`}
>
{filteredProducts.map((product) => (
<ProductCard key={product.id} product={product} />
<ProductCard key={product.id} product={product} viewMode={viewMode} />
))}
</div>
</>

View File

@@ -1,21 +1,29 @@
import React, { useState, useEffect } from 'react';
import { useParams, Link, useNavigate } from 'react-router-dom';
import axios from 'axios';
import { ArrowLeft, Clock, Calendar, Check, Phone, Mail, User } from 'lucide-react';
import { Button } from '../components/ui/button';
import { Badge } from '../components/ui/badge';
import { Input } from '../components/ui/input';
import { Textarea } from '../components/ui/textarea';
import { Label } from '../components/ui/label';
import { Separator } from '../components/ui/separator';
import React, { useState, useEffect } from "react";
import { useParams, Link, useNavigate } from "react-router-dom";
import axios from "axios";
import {
ArrowLeft,
Clock,
Calendar,
Check,
Phone,
Mail,
User,
} from "lucide-react";
import { Button } from "../components/ui/button";
import { Badge } from "../components/ui/badge";
import { Input } from "../components/ui/input";
import { Textarea } from "../components/ui/textarea";
import { Label } from "../components/ui/label";
import { Separator } from "../components/ui/separator";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '../components/ui/dialog';
import { toast } from 'sonner';
} from "../components/ui/dialog";
import { toast } from "sonner";
const API = `${process.env.REACT_APP_BACKEND_URL}/api`;
@@ -27,25 +35,48 @@ const ServiceDetail = () => {
const [bookingOpen, setBookingOpen] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [formData, setFormData] = useState({
name: '',
email: '',
phone: '',
preferred_date: '',
notes: ''
name: "",
email: "",
phone: "",
preferred_date: "",
notes: "",
});
const fetchService = async (silent = false) => {
try {
const response = await axios.get(`${API}/services/${id}`);
setService(response.data);
} catch (error) {
console.error("Failed to fetch service:", error);
} finally {
if (!silent) setLoading(false);
}
};
useEffect(() => {
const fetchService = async () => {
try {
const response = await axios.get(`${API}/services/${id}`);
setService(response.data);
} catch (error) {
console.error('Failed to fetch service:', error);
} finally {
setLoading(false);
fetchService();
}, [id]);
// Auto-refresh every 5 seconds for real-time updates
useEffect(() => {
const interval = setInterval(() => {
fetchService(true);
}, 5000);
return () => clearInterval(interval);
}, [id]);
// Refresh when user returns to the tab
useEffect(() => {
const handleVisibilityChange = () => {
if (!document.hidden) {
fetchService();
}
};
fetchService();
document.addEventListener("visibilitychange", handleVisibilityChange);
return () =>
document.removeEventListener("visibilitychange", handleVisibilityChange);
}, [id]);
const handleSubmit = async (e) => {
@@ -55,13 +86,21 @@ const ServiceDetail = () => {
try {
await axios.post(`${API}/services/book`, {
service_id: service.id,
...formData
...formData,
});
toast.success('Booking submitted successfully! We will contact you soon.');
toast.success(
"Booking submitted successfully! We will contact you soon.",
);
setBookingOpen(false);
setFormData({ name: '', email: '', phone: '', preferred_date: '', notes: '' });
setFormData({
name: "",
email: "",
phone: "",
preferred_date: "",
notes: "",
});
} catch (error) {
toast.error('Failed to submit booking. Please try again.');
toast.error("Failed to submit booking. Please try again.");
} finally {
setSubmitting(false);
}
@@ -85,7 +124,9 @@ const ServiceDetail = () => {
return (
<div className="min-h-screen py-8 md:py-12">
<div className="max-w-4xl mx-auto px-4 md:px-8 text-center py-16">
<h2 className="text-2xl font-bold mb-4 font-['Outfit']">Service not found</h2>
<h2 className="text-2xl font-bold mb-4 font-['Outfit']">
Service not found
</h2>
<Link to="/services">
<Button className="rounded-full">Back to Services</Button>
</Link>
@@ -124,26 +165,33 @@ const ServiceDetail = () => {
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div className="lg:col-span-2 space-y-6">
<div>
<h1 className="text-3xl md:text-4xl font-bold font-['Outfit'] mb-4" data-testid="service-title">
<h1
className="text-3xl md:text-4xl font-bold font-['Outfit'] mb-4"
data-testid="service-title"
>
{service.name}
</h1>
<p className="text-lg text-muted-foreground leading-relaxed" data-testid="service-description">
{service.description}
</p>
<div
className="text-lg text-muted-foreground leading-relaxed prose prose-lg max-w-none"
data-testid="service-description"
dangerouslySetInnerHTML={{ __html: service.description }}
/>
</div>
<Separator />
{/* What's Included */}
<div>
<h3 className="text-xl font-semibold mb-4 font-['Outfit']">What's Included</h3>
<h3 className="text-xl font-semibold mb-4 font-['Outfit']">
What's Included
</h3>
<ul className="space-y-3">
{[
'Free diagnostic assessment',
'Quality replacement parts (if needed)',
'Professional service by certified technicians',
'30-day warranty on all repairs',
'Post-service support'
"Free diagnostic assessment",
"Quality replacement parts (if needed)",
"Professional service by certified technicians",
"30-day warranty on all repairs",
"Post-service support",
].map((item, idx) => (
<li key={idx} className="flex items-center gap-3">
<div className="w-5 h-5 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0">
@@ -159,18 +207,37 @@ const ServiceDetail = () => {
{/* Process */}
<div>
<h3 className="text-xl font-semibold mb-4 font-['Outfit']">How It Works</h3>
<h3 className="text-xl font-semibold mb-4 font-['Outfit']">
How It Works
</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{[
{ step: '1', title: 'Book', desc: 'Schedule your appointment online' },
{ step: '2', title: 'Drop Off', desc: 'Bring your device to our store' },
{ step: '3', title: 'Pick Up', desc: 'Get your device back fixed' }
{
step: "1",
title: "Book",
desc: "Schedule your appointment online",
},
{
step: "2",
title: "Drop Off",
desc: "Bring your device to our store",
},
{
step: "3",
title: "Pick Up",
desc: "Get your device back fixed",
},
].map((item, idx) => (
<div key={idx} className="text-center p-4 rounded-xl bg-muted/50">
<div
key={idx}
className="text-center p-4 rounded-xl bg-muted/50"
>
<div className="w-10 h-10 mx-auto mb-3 rounded-full bg-primary text-primary-foreground flex items-center justify-center font-bold">
{item.step}
</div>
<h4 className="font-semibold mb-1 font-['Outfit']">{item.title}</h4>
<h4 className="font-semibold mb-1 font-['Outfit']">
{item.title}
</h4>
<p className="text-sm text-muted-foreground">{item.desc}</p>
</div>
))}
@@ -182,9 +249,16 @@ const ServiceDetail = () => {
<div className="lg:col-span-1">
<div className="sticky top-24 border border-border rounded-2xl p-6 bg-card space-y-6">
<div>
<span className="text-sm text-muted-foreground">Starting from</span>
<p className="text-3xl font-bold font-['Outfit']" data-testid="service-price">
${service.price.toFixed(2)}
<span className="text-sm text-muted-foreground">
{service.price > 0 ? "Starting from" : "Price"}
</span>
<p
className="text-3xl font-bold font-['Outfit']"
data-testid="service-price"
>
{service.price > 0
? `$${service.price.toFixed(2)}`
: "Contact for quote"}
</p>
</div>
@@ -195,13 +269,19 @@ const ServiceDetail = () => {
<Dialog open={bookingOpen} onOpenChange={setBookingOpen}>
<DialogTrigger asChild>
<Button className="w-full rounded-full btn-press" size="lg" data-testid="book-now-button">
<Button
className="w-full rounded-full btn-press"
size="lg"
data-testid="book-now-button"
>
Book Now
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="font-['Outfit']">Book {service.name}</DialogTitle>
<DialogTitle className="font-['Outfit']">
Book {service.name}
</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4 mt-4">
<div className="space-y-2">
@@ -213,7 +293,9 @@ const ServiceDetail = () => {
placeholder="John Doe"
className="pl-10"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
onChange={(e) =>
setFormData({ ...formData, name: e.target.value })
}
required
data-testid="booking-name"
/>
@@ -230,7 +312,9 @@ const ServiceDetail = () => {
placeholder="john@example.com"
className="pl-10"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
onChange={(e) =>
setFormData({ ...formData, email: e.target.value })
}
required
data-testid="booking-email"
/>
@@ -247,7 +331,9 @@ const ServiceDetail = () => {
placeholder="+1 234 567 890"
className="pl-10"
value={formData.phone}
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
onChange={(e) =>
setFormData({ ...formData, phone: e.target.value })
}
required
data-testid="booking-phone"
/>
@@ -263,7 +349,12 @@ const ServiceDetail = () => {
type="date"
className="pl-10"
value={formData.preferred_date}
onChange={(e) => setFormData({ ...formData, preferred_date: e.target.value })}
onChange={(e) =>
setFormData({
...formData,
preferred_date: e.target.value,
})
}
required
data-testid="booking-date"
/>
@@ -276,18 +367,20 @@ const ServiceDetail = () => {
id="notes"
placeholder="Describe your issue..."
value={formData.notes}
onChange={(e) => setFormData({ ...formData, notes: e.target.value })}
onChange={(e) =>
setFormData({ ...formData, notes: e.target.value })
}
data-testid="booking-notes"
/>
</div>
<Button
type="submit"
className="w-full rounded-full"
<Button
type="submit"
className="w-full rounded-full"
disabled={submitting}
data-testid="submit-booking"
>
{submitting ? 'Submitting...' : 'Submit Booking'}
{submitting ? "Submitting..." : "Submit Booking"}
</Button>
</form>
</DialogContent>

View File

@@ -38,23 +38,36 @@ const Services = () => {
fetchServices();
}, [activeCategory]);
const fetchServices = async () => {
setLoading(true);
// Auto-refresh every 5 seconds for real-time updates
useEffect(() => {
const interval = setInterval(() => {
fetchServices(true);
}, 5000);
return () => clearInterval(interval);
}, [activeCategory]);
// Refresh when user returns to the tab
useEffect(() => {
const handleVisibilityChange = () => {
if (!document.hidden) {
fetchServices();
}
};
document.addEventListener("visibilitychange", handleVisibilityChange);
return () =>
document.removeEventListener("visibilitychange", handleVisibilityChange);
}, [activeCategory]);
const fetchServices = async (silent = false) => {
if (!silent) setLoading(true);
try {
const params =
activeCategory !== "all" ? `?category=${activeCategory}` : "";
const cacheKey = `services-${activeCategory}`;
const cached = getCached(cacheKey);
if (cached) {
setServices(cached);
setLoading(false);
return;
}
const response = await axios.get(`${API}/services${params}`);
setServices(response.data);
setCache(cacheKey, response.data);
} catch (error) {
console.error("Failed to fetch services:", error);
} finally {
@@ -63,7 +76,6 @@ const Services = () => {
};
const stats = [
{ value: "10K+", label: "Devices Repaired" },
{ value: "98%", label: "Success Rate" },
{ value: "24h", label: "Avg Turnaround" },
{ value: "5 Star", label: "Customer Rating" },
@@ -85,12 +97,14 @@ const Services = () => {
<span className="text-muted-foreground">Tech Solutions</span>
</h1>
<p className="text-lg text-muted-foreground max-w-lg">
From screen repairs to data recovery, our certified technicians
provide professional solutions for all your tech needs.
From screen repairs to advanced data recovery, we provide
reliable and professional solutions for all your technology
needs. Our services cover both hardware and software, ensuring
your devices run smoothly, securely, and efficiently.
</p>
{/* Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 pt-4">
<div className="grid grid-cols-3 gap-4 pt-4">
{stats.map((stat, idx) => (
<div
key={idx}
@@ -119,6 +133,57 @@ const Services = () => {
</div>
</section>
{/* Specializations Section */}
<section className="py-16 bg-card border-b border-border">
<div className="max-w-7xl mx-auto px-4 md:px-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
<div>
<h2 className="text-2xl md:text-3xl font-bold font-['Outfit'] mb-6">
We Specialize In
</h2>
<ul className="space-y-3">
{[
"Computer and laptop repairs",
"Screen replacements and hardware troubleshooting",
"Software installation and system configuration",
"Windows 10 and Windows 11 repair, recovery, and optimization",
"Operating system reinstallation and OS-level repairs",
].map((item, idx) => (
<li key={idx} className="flex items-start gap-3">
<div className="w-2 h-2 rounded-full bg-primary mt-2 flex-shrink-0" />
<span className="text-muted-foreground">{item}</span>
</li>
))}
</ul>
</div>
<div>
<h2 className="text-2xl md:text-3xl font-bold font-['Outfit'] mb-6 lg:invisible">
&nbsp;
</h2>
<ul className="space-y-3">
{[
"Microsoft Office installation, activation, and setup",
"Adobe software installation and configuration",
"QuickBooks POS 2019 installation, setup, and troubleshooting",
"Server management and system synchronization across multiple devices",
"Data backup, recovery, and system cleanup",
].map((item, idx) => (
<li key={idx} className="flex items-start gap-3">
<div className="w-2 h-2 rounded-full bg-primary mt-2 flex-shrink-0" />
<span className="text-muted-foreground">{item}</span>
</li>
))}
</ul>
</div>
</div>
<p className="text-center text-muted-foreground mt-8 max-w-3xl mx-auto">
Our certified technicians provide professional solutions for all
your tech needswhether it's a single device repair or managing
systems across your entire business.
</p>
</div>
</section>
{/* Categories */}
<section className="py-8 border-b border-border sticky top-16 bg-background/95 backdrop-blur-sm z-40">
<div className="max-w-7xl mx-auto px-4 md:px-8">

View File

@@ -0,0 +1,124 @@
import React, { useEffect, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import { CheckCircle, XCircle, Loader2 } from "lucide-react";
const API = process.env.REACT_APP_API_URL || "http://localhost:8181/api";
const VerifyEmail = () => {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const [status, setStatus] = useState("verifying"); // verifying, success, error
const [message, setMessage] = useState("Verifying your email...");
useEffect(() => {
const verifyEmail = async () => {
const token = searchParams.get("token");
if (!token) {
setStatus("error");
setMessage(
"Invalid verification link. Please check your email and try again.",
);
return;
}
try {
const response = await fetch(
`${API}/auth/verify-email?token=${encodeURIComponent(token)}`,
);
const data = await response.json();
if (response.ok) {
setStatus("success");
setMessage("Email verified successfully! Redirecting to login...");
// Redirect to login after 3 seconds
setTimeout(() => {
navigate("/login");
}, 3000);
} else {
setStatus("error");
setMessage(
data.detail ||
"Verification failed. The link may be expired or invalid.",
);
}
} catch (error) {
console.error("Verification error:", error);
setStatus("error");
setMessage(
"An error occurred during verification. Please try again later.",
);
}
};
verifyEmail();
}, [searchParams, navigate]);
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center px-4">
<div className="max-w-md w-full bg-white rounded-2xl shadow-xl p-8">
<div className="text-center">
{/* Status Icon */}
<div className="mb-6 flex justify-center">
{status === "verifying" && (
<Loader2 className="w-16 h-16 text-blue-600 animate-spin" />
)}
{status === "success" && (
<CheckCircle className="w-16 h-16 text-green-600" />
)}
{status === "error" && (
<XCircle className="w-16 h-16 text-red-600" />
)}
</div>
{/* Title */}
<h1 className="text-2xl font-bold text-gray-900 mb-3">
{status === "verifying" && "Verifying Email"}
{status === "success" && "Email Verified!"}
{status === "error" && "Verification Failed"}
</h1>
{/* Message */}
<p className="text-gray-600 mb-6">{message}</p>
{/* Actions */}
{status === "error" && (
<div className="space-y-3">
<button
onClick={() => navigate("/login")}
className="w-full bg-blue-600 text-white py-3 px-4 rounded-lg font-medium hover:bg-blue-700 transition-colors"
>
Back to Login
</button>
<button
onClick={() =>
(window.location.href = "mailto:prompttechbz@gmail.com")
}
className="w-full bg-gray-100 text-gray-700 py-3 px-4 rounded-lg font-medium hover:bg-gray-200 transition-colors"
>
Contact Support
</button>
</div>
)}
{status === "success" && (
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<p className="text-green-800 text-sm">
Your account is now active
<br /> You can now log in and start shopping
</p>
</div>
)}
</div>
{/* Footer */}
<div className="mt-8 pt-6 border-t border-gray-200 text-center">
<p className="text-sm text-gray-500">PromptTech Solution</p>
</div>
</div>
</div>
);
};
export default VerifyEmail;

View File

@@ -1,6 +1,6 @@
// Simple in-memory cache for API responses
const cache = new Map();
const CACHE_DURATION = 60000; // 60 seconds
const CACHE_DURATION = 30000; // 30 seconds (reduced from 60)
export const getCached = (key) => {
const cached = cache.get(key);
@@ -24,7 +24,19 @@ export const setCache = (key, data) => {
export const clearCache = (key) => {
if (key) {
cache.delete(key);
// If key ends with a dash, clear all keys starting with that prefix
if (key.endsWith("-")) {
const prefix = key;
const keysToDelete = [];
for (const [cacheKey] of cache) {
if (cacheKey.startsWith(prefix)) {
keysToDelete.push(cacheKey);
}
}
keysToDelete.forEach((k) => cache.delete(k));
} else {
cache.delete(key);
}
} else {
cache.clear();
}

File diff suppressed because it is too large Load Diff

View File

@@ -458,3 +458,760 @@
2026-01-13 18:39:43 -06:00: INFO: 127.0.0.1:43056 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-01-13 18:39:49 -06:00: INFO: 127.0.0.1:43062 - "OPTIONS /api/admin/inventory HTTP/1.1" 200 OK
2026-01-13 18:39:49 -06:00: INFO: 127.0.0.1:43062 - "GET /api/admin/inventory HTTP/1.1" 200 OK
2026-02-01 16:30:11 -06:00: INFO: 127.0.0.1:3048 - "GET /api/health HTTP/1.1" 200 OK
2026-02-01 16:33:27 -06:00: INFO: 127.0.0.1:14622 - "GET /api/health HTTP/1.1" 200 OK
2026-02-01 16:37:50 -06:00: INFO: 127.0.0.1:60700 - "GET /api/health HTTP/1.1" 200 OK
2026-02-01 16:39:10 -06:00: INFO: 127.0.0.1:8654 - "GET /api/health HTTP/1.1" 200 OK
2026-02-01 16:39:29 -06:00: INFO: 127.0.0.1:57386 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 16:39:29 -06:00: INFO: 127.0.0.1:57408 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 16:39:29 -06:00: INFO: 127.0.0.1:57408 - "GET /api/cart HTTP/1.1" 401 Unauthorized
2026-02-01 16:39:29 -06:00: INFO: 127.0.0.1:57402 - "GET /api/products HTTP/1.1" 200 OK
2026-02-01 16:39:29 -06:00: INFO: 127.0.0.1:57416 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 16:39:29 -06:00: INFO: 127.0.0.1:57408 - "GET /api/auth/me HTTP/1.1" 401 Unauthorized
2026-02-01 16:39:33 -06:00: INFO: 127.0.0.1:57408 - "GET /api/products HTTP/1.1" 200 OK
2026-02-01 16:39:33 -06:00: INFO: 127.0.0.1:15360 - "GET /uploads/products/da21b947-83d7-433c-a11d-3fb170721821.jpg HTTP/1.1" 200 OK
2026-02-01 16:41:52 -06:00: INFO: 127.0.0.1:53088 - "GET /api/health HTTP/1.1" 200 OK
2026-02-01 16:43:26 -06:00: INFO: 127.0.0.1:25110 - "GET /api/products HTTP/1.1" 200 OK
2026-02-01 16:43:29 -06:00: INFO: 127.0.0.1:25110 - "GET /api/products HTTP/1.1" 200 OK
2026-02-01 16:43:29 -06:00: INFO: 127.0.0.1:25122 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 16:43:32 -06:00: INFO: 127.0.0.1:25122 - "GET /api/products/1dd042e3-963d-430f-89c2-1eee235b345a HTTP/1.1" 200 OK
2026-02-01 16:56:22 -06:00: INFO: 127.0.0.1:2068 - "OPTIONS /api/auth/login HTTP/1.1" 200 OK
2026-02-01 16:56:22 -06:00: INFO: 127.0.0.1:2068 - "POST /api/auth/login HTTP/1.1" 401 Unauthorized
2026-02-01 16:56:30 -06:00: INFO: 127.0.0.1:2078 - "POST /api/auth/login HTTP/1.1" 401 Unauthorized
2026-02-01 16:57:19 -06:00: INFO: 127.0.0.1:38080 - "POST /api/auth/login HTTP/1.1" 401 Unauthorized
2026-02-01 16:57:35 -06:00: INFO: 127.0.0.1:38088 - "POST /api/auth/login HTTP/1.1" 401 Unauthorized
2026-02-01 16:58:01 -06:00: INFO: 127.0.0.1:30140 - "POST /api/auth/login HTTP/1.1" 401 Unauthorized
2026-02-01 17:06:31 -06:00: INFO: 127.0.0.1:48510 - "GET /api/health HTTP/1.1" 200 OK
2026-02-01 17:06:41 -06:00: INFO: 127.0.0.1:45960 - "POST /api/auth/login HTTP/1.1" 401 Unauthorized
2026-02-01 17:06:59 -06:00: INFO: 127.0.0.1:28268 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 17:07:03 -06:00: INFO: 127.0.0.1:59896 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 17:07:15 -06:00: INFO: 127.0.0.1:45226 - "OPTIONS /api/auth/login HTTP/1.1" 200 OK
2026-02-01 17:07:15 -06:00: INFO: 127.0.0.1:45238 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 17:07:15 -06:00: INFO: 127.0.0.1:45238 - "GET /api/products HTTP/1.1" 200 OK
2026-02-01 17:07:15 -06:00: INFO: 127.0.0.1:45248 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 17:07:15 -06:00: INFO: 127.0.0.1:45238 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 17:07:15 -06:00: INFO: 127.0.0.1:45248 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 17:07:24 -06:00: INFO: 127.0.0.1:45262 - "OPTIONS /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 17:07:24 -06:00: INFO: 127.0.0.1:8480 - "OPTIONS /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 17:07:24 -06:00: INFO: 127.0.0.1:8480 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 17:07:25 -06:00: INFO: 127.0.0.1:8496 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 17:07:26 -06:00: INFO: 127.0.0.1:8496 - "OPTIONS /api/admin/users?skip=0&limit=20&search=&role=&status= HTTP/1.1" 200 OK
2026-02-01 17:07:26 -06:00: INFO: 127.0.0.1:8496 - "GET /api/admin/users?skip=0&limit=20&search=&role=&status= HTTP/1.1" 200 OK
2026-02-01 17:07:56 -06:00: INFO: 127.0.0.1:58692 - "OPTIONS /api/admin/users HTTP/1.1" 200 OK
2026-02-01 17:07:57 -06:00: INFO: 127.0.0.1:58704 - "POST /api/admin/users HTTP/1.1" 200 OK
2026-02-01 17:07:57 -06:00: INFO: 127.0.0.1:58704 - "GET /api/admin/users?skip=0&limit=20&search=&role=&status= HTTP/1.1" 200 OK
2026-02-01 17:08:13 -06:00: INFO: 127.0.0.1:58706 - "POST /api/auth/login HTTP/1.1" 401 Unauthorized
2026-02-01 17:08:21 -06:00: INFO: 127.0.0.1:63300 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 17:08:21 -06:00: INFO: 127.0.0.1:63300 - "GET /api/products HTTP/1.1" 200 OK
2026-02-01 17:08:21 -06:00: INFO: 127.0.0.1:63308 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 17:08:21 -06:00: INFO: 127.0.0.1:63308 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 17:08:26 -06:00: INFO: 127.0.0.1:63330 - "GET /api/products HTTP/1.1" 200 OK
2026-02-01 17:08:27 -06:00: INFO: 127.0.0.1:63330 - "GET /api/products/1dd042e3-963d-430f-89c2-1eee235b345a HTTP/1.1" 200 OK
2026-02-01 17:08:30 -06:00: INFO: 127.0.0.1:63316 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 17:08:30 -06:00: INFO: 127.0.0.1:63330 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 17:08:32 -06:00: INFO: 127.0.0.1:63330 - "OPTIONS /api/admin/products?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 17:08:32 -06:00: INFO: 127.0.0.1:63330 - "GET /api/admin/products?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 17:08:34 -06:00: INFO: 127.0.0.1:63330 - "OPTIONS /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 17:08:34 -06:00: INFO: 127.0.0.1:63316 - "GET /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 17:08:35 -06:00: INFO: 127.0.0.1:63022 - "GET /uploads/products/test.jpg HTTP/1.1" 404 Not Found
2026-02-01 17:11:18 -06:00: INFO: 127.0.0.1:10630 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 17:11:18 -06:00: INFO: 127.0.0.1:10632 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 17:11:18 -06:00: INFO: 127.0.0.1:10632 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 17:11:18 -06:00: INFO: 127.0.0.1:10632 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 17:16:28 -06:00: INFO: 127.0.0.1:51272 - "GET /api/products HTTP/1.1" 200 OK
2026-02-01 17:16:28 -06:00: INFO: 127.0.0.1:51280 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 17:17:08 -06:00: INFO: 127.0.0.1:51284 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 17:17:08 -06:00: INFO: 127.0.0.1:51284 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 17:17:08 -06:00: INFO: 127.0.0.1:22960 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 17:17:08 -06:00: INFO: 127.0.0.1:22980 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 17:19:07 -06:00: INFO: 127.0.0.1:7002 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 17:19:07 -06:00: INFO: 127.0.0.1:7006 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 17:19:07 -06:00: INFO: 127.0.0.1:7002 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 17:19:07 -06:00: INFO: 127.0.0.1:7006 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 17:26:28 -06:00: INFO: 127.0.0.1:13572 - "OPTIONS /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 17:26:28 -06:00: INFO: 127.0.0.1:13582 - "OPTIONS /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 17:26:29 -06:00: INFO: 127.0.0.1:13572 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 17:26:29 -06:00: INFO: 127.0.0.1:13572 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 17:32:45 -06:00: INFO: 127.0.0.1:15824 - "GET /api/health HTTP/1.1" 200 OK
2026-02-01 17:32:57 -06:00: INFO: 127.0.0.1:30510 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 17:32:58 -06:00: INFO: 127.0.0.1:30512 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 17:32:58 -06:00: INFO: 127.0.0.1:30510 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 17:32:58 -06:00: INFO: 127.0.0.1:30512 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 17:32:58 -06:00: INFO: 127.0.0.1:30512 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 17:32:58 -06:00: INFO: 127.0.0.1:30510 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 17:33:00 -06:00: INFO: 127.0.0.1:30510 - "OPTIONS /api/media?page=1&limit=24 HTTP/1.1" 200 OK
2026-02-01 17:33:00 -06:00: INFO: 127.0.0.1:30510 - "GET /api/media?page=1&limit=24 HTTP/1.1" 200 OK
2026-02-01 17:33:14 -06:00: INFO: 127.0.0.1:30516 - "OPTIONS /api/media/upload HTTP/1.1" 200 OK
2026-02-01 17:33:14 -06:00: INFO: 127.0.0.1:30516 - "POST /api/media/upload HTTP/1.1" 500 Internal Server Error
2026-02-01 17:33:38 -06:00: INFO: 127.0.0.1:30518 - "POST /api/media/upload HTTP/1.1" 500 Internal Server Error
2026-02-01 17:33:42 -06:00: INFO: 127.0.0.1:30518 - "POST /api/media/upload HTTP/1.1" 500 Internal Server Error
2026-02-01 17:34:45 -06:00: INFO: 127.0.0.1:13614 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 17:34:45 -06:00: INFO: 127.0.0.1:13618 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 17:34:45 -06:00: INFO: 127.0.0.1:13618 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 17:34:45 -06:00: INFO: 127.0.0.1:13614 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 17:34:45 -06:00: INFO: 127.0.0.1:13618 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 17:34:45 -06:00: INFO: 127.0.0.1:13614 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 17:36:00 -06:00: INFO: 127.0.0.1:35118 - "GET /api/health HTTP/1.1" 200 OK
2026-02-01 17:36:09 -06:00: INFO: 127.0.0.1:38080 - "GET /api/health HTTP/1.1" 200 OK
2026-02-01 17:36:20 -06:00: INFO: 127.0.0.1:56340 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 17:36:20 -06:00: INFO: 127.0.0.1:56356 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 17:36:20 -06:00: INFO: 127.0.0.1:56340 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 17:36:20 -06:00: INFO: 127.0.0.1:56356 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 17:36:20 -06:00: INFO: 127.0.0.1:56340 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 17:36:20 -06:00: INFO: 127.0.0.1:56356 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 17:36:21 -06:00: INFO: 127.0.0.1:56356 - "GET /api/media?page=1&limit=24 HTTP/1.1" 200 OK
2026-02-01 17:36:25 -06:00: INFO: 127.0.0.1:56356 - "POST /api/media/upload HTTP/1.1" 200 OK
2026-02-01 17:36:25 -06:00: INFO: 127.0.0.1:56356 - "GET /api/media?page=1&limit=24 HTTP/1.1" 200 OK
2026-02-01 17:36:25 -06:00: INFO: 127.0.0.1:53026 - "GET /uploads/media/aa5bcc15-3b1e-4ed8-8708-1a3dceb9494d.jpg HTTP/1.1" 200 OK
2026-02-01 17:36:42 -06:00: INFO: 127.0.0.1:56366 - "OPTIONS /api/media/50704a79-caf9-437e-8047-a9ab3ad944aa HTTP/1.1" 200 OK
2026-02-01 17:36:42 -06:00: INFO: 127.0.0.1:56366 - "PUT /api/media/50704a79-caf9-437e-8047-a9ab3ad944aa HTTP/1.1" 200 OK
2026-02-01 17:36:42 -06:00: INFO: 127.0.0.1:56366 - "GET /api/media?page=1&limit=24 HTTP/1.1" 200 OK
2026-02-01 17:36:50 -06:00: INFO: 127.0.0.1:45298 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 17:36:50 -06:00: INFO: 127.0.0.1:45314 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 17:36:50 -06:00: INFO: 127.0.0.1:45298 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 17:36:50 -06:00: INFO: 127.0.0.1:45314 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 17:36:50 -06:00: INFO: 127.0.0.1:45314 - "OPTIONS /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 17:36:50 -06:00: INFO: 127.0.0.1:45298 - "OPTIONS /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 17:36:50 -06:00: INFO: 127.0.0.1:45298 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 17:36:50 -06:00: INFO: 127.0.0.1:45316 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 17:36:52 -06:00: INFO: 127.0.0.1:45316 - "GET /api/media?page=1&limit=24 HTTP/1.1" 200 OK
2026-02-01 17:36:52 -06:00: INFO: 127.0.0.1:53038 - "GET /uploads/media/aa5bcc15-3b1e-4ed8-8708-1a3dceb9494d.jpg HTTP/1.1" 304 Not Modified
2026-02-01 17:37:09 -06:00: INFO: 127.0.0.1:45318 - "PUT /api/media/50704a79-caf9-437e-8047-a9ab3ad944aa HTTP/1.1" 200 OK
2026-02-01 17:37:09 -06:00: INFO: 127.0.0.1:45318 - "GET /api/media?page=1&limit=24 HTTP/1.1" 200 OK
2026-02-01 17:37:53 -06:00: INFO: 127.0.0.1:12800 - "OPTIONS /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 17:37:53 -06:00: INFO: 127.0.0.1:12808 - "OPTIONS /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 17:37:53 -06:00: INFO: 127.0.0.1:12824 - "OPTIONS /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 17:37:53 -06:00: INFO: 127.0.0.1:12800 - "GET /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 17:37:53 -06:00: INFO: 127.0.0.1:12824 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 17:37:53 -06:00: INFO: 127.0.0.1:12808 - "GET /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 17:39:15 -06:00: INFO: 127.0.0.1:45602 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 17:39:15 -06:00: INFO: 127.0.0.1:45610 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 17:39:15 -06:00: INFO: 127.0.0.1:45602 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 17:39:15 -06:00: INFO: 127.0.0.1:45602 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 17:39:15 -06:00: INFO: 127.0.0.1:45602 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 17:39:15 -06:00: INFO: 127.0.0.1:45614 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 17:39:17 -06:00: INFO: 127.0.0.1:45614 - "GET /api/media?page=1&limit=24 HTTP/1.1" 200 OK
2026-02-01 17:39:18 -06:00: INFO: 127.0.0.1:45648 - "GET /uploads/media/aa5bcc15-3b1e-4ed8-8708-1a3dceb9494d.jpg HTTP/1.1" 304 Not Modified
2026-02-01 17:44:50 -06:00: INFO: 127.0.0.1:55690 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 17:44:50 -06:00: INFO: 127.0.0.1:55698 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 17:44:50 -06:00: INFO: 127.0.0.1:55698 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 17:44:50 -06:00: INFO: 127.0.0.1:55698 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 17:44:50 -06:00: INFO: 127.0.0.1:55698 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 17:44:50 -06:00: INFO: 127.0.0.1:55700 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 17:44:52 -06:00: INFO: 127.0.0.1:55700 - "OPTIONS /api/media?page=1&limit=24 HTTP/1.1" 200 OK
2026-02-01 17:44:52 -06:00: INFO: 127.0.0.1:55698 - "GET /api/media?page=1&limit=24 HTTP/1.1" 200 OK
2026-02-01 17:44:52 -06:00: INFO: 127.0.0.1:23150 - "GET /uploads/media/aa5bcc15-3b1e-4ed8-8708-1a3dceb9494d.jpg HTTP/1.1" 304 Not Modified
2026-02-01 17:45:20 -06:00: INFO: 127.0.0.1:55704 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 17:45:20 -06:00: INFO: 127.0.0.1:55714 - "GET /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 17:45:20 -06:00: INFO: 127.0.0.1:55722 - "GET /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 17:45:35 -06:00: INFO: 127.0.0.1:38428 - "GET /api/media HTTP/1.1" 403 Forbidden
2026-02-01 17:45:42 -06:00: INFO: 127.0.0.1:50368 - "GET /api/media HTTP/1.1" 403 Forbidden
2026-02-01 17:46:05 -06:00: INFO: 127.0.0.1:36942 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 17:46:05 -06:00: INFO: 127.0.0.1:36926 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 17:46:06 -06:00: INFO: 127.0.0.1:36942 - "GET /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 17:46:06 -06:00: INFO: 127.0.0.1:36926 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 17:46:06 -06:00: INFO: 127.0.0.1:36952 - "GET /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 17:46:53 -06:00: INFO: 127.0.0.1:15764 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 17:46:53 -06:00: INFO: 127.0.0.1:15768 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 17:46:53 -06:00: INFO: 127.0.0.1:15768 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 17:46:53 -06:00: INFO: 127.0.0.1:15782 - "GET /uploads/media/aa5bcc15-3b1e-4ed8-8708-1a3dceb9494d.jpg HTTP/1.1" 200 OK
2026-02-01 17:46:53 -06:00: INFO: 127.0.0.1:15764 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 17:49:40 -06:00: INFO: 127.0.0.1:17196 - "HEAD /uploads/media/aa5bcc15-3b1e-4ed8-8708-1a3dceb9494d.jpg HTTP/1.1" 200 OK
2026-02-01 17:51:07 -06:00: INFO: ::1:0 - "HEAD /uploads/media/test.txt HTTP/1.1" 404 Not Found
2026-02-01 17:56:52 -06:00: INFO: ::1:0 - "HEAD /uploads/media/aa5bcc15-3b1e-4ed8-8708-1a3dceb9494d.jpg HTTP/1.1" 200 OK
2026-02-01 17:57:24 -06:00: INFO: 127.0.0.1:17680 - "GET /uploads/media/aa5bcc15-3b1e-4ed8-8708-1a3dceb9494d.jpg HTTP/1.1" 304 Not Modified
2026-02-01 17:57:26 -06:00: INFO: 127.0.0.1:17694 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 17:57:26 -06:00: INFO: 127.0.0.1:17680 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 17:57:26 -06:00: INFO: None:0 - "GET /uploads/media/aa5bcc15-3b1e-4ed8-8708-1a3dceb9494d.jpg HTTP/1.1" 200 OK
2026-02-01 17:57:26 -06:00: INFO: 127.0.0.1:17680 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 17:57:26 -06:00: INFO: 127.0.0.1:17694 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 18:05:46 -06:00: INFO: 127.0.0.1:59026 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 18:05:46 -06:00: INFO: 127.0.0.1:59022 - "GET /api/products HTTP/1.1" 200 OK
2026-02-01 18:08:10 -06:00: INFO: 127.0.0.1:45434 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 18:08:10 -06:00: INFO: 127.0.0.1:45438 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 18:08:11 -06:00: INFO: 127.0.0.1:45434 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 18:08:11 -06:00: INFO: 127.0.0.1:45438 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 18:10:21 -06:00: INFO: 127.0.0.1:57524 - "OPTIONS /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 18:10:21 -06:00: INFO: 127.0.0.1:57540 - "OPTIONS /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 18:10:21 -06:00: INFO: 127.0.0.1:57540 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 18:10:21 -06:00: INFO: 127.0.0.1:57540 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 18:10:25 -06:00: INFO: 127.0.0.1:57540 - "OPTIONS /api/admin/inventory HTTP/1.1" 200 OK
2026-02-01 18:10:25 -06:00: INFO: 127.0.0.1:57540 - "GET /api/admin/inventory HTTP/1.1" 200 OK
2026-02-01 18:10:27 -06:00: INFO: 127.0.0.1:57540 - "OPTIONS /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 18:10:27 -06:00: INFO: 127.0.0.1:57540 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 18:10:28 -06:00: INFO: 127.0.0.1:57540 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 18:10:30 -06:00: INFO: 127.0.0.1:57540 - "OPTIONS /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 18:10:30 -06:00: INFO: 127.0.0.1:57540 - "GET /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 18:10:31 -06:00: INFO: 127.0.0.1:2606 - "GET /uploads/products/test.jpg HTTP/1.1" 404 Not Found
2026-02-01 18:10:31 -06:00: INFO: 127.0.0.1:57540 - "OPTIONS /api/admin/products?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 18:10:31 -06:00: INFO: 127.0.0.1:2580 - "GET /api/admin/products?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 18:10:33 -06:00: INFO: 127.0.0.1:2584 - "OPTIONS /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 18:10:33 -06:00: INFO: 127.0.0.1:2580 - "OPTIONS /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 18:10:33 -06:00: INFO: 127.0.0.1:2592 - "OPTIONS /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 18:10:33 -06:00: INFO: 127.0.0.1:2580 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 18:10:33 -06:00: INFO: 127.0.0.1:2584 - "GET /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 18:10:33 -06:00: INFO: 127.0.0.1:2592 - "GET /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 18:16:07 -06:00: INFO: 127.0.0.1:37524 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 18:16:07 -06:00: INFO: 127.0.0.1:37528 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 18:16:07 -06:00: INFO: 127.0.0.1:37524 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 18:16:07 -06:00: INFO: 127.0.0.1:37528 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 18:16:07 -06:00: INFO: 127.0.0.1:37524 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 18:16:07 -06:00: INFO: 127.0.0.1:37528 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 18:16:08 -06:00: INFO: 127.0.0.1:37524 - "GET /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 18:16:08 -06:00: INFO: 127.0.0.1:37528 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 18:16:08 -06:00: INFO: 127.0.0.1:37540 - "GET /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 18:16:12 -06:00: INFO: 127.0.0.1:37540 - "OPTIONS /api/admin/about/team/780a7413-d421-4a7a-8c51-f45368215490 HTTP/1.1" 200 OK
2026-02-01 18:16:12 -06:00: INFO: 127.0.0.1:37540 - "DELETE /api/admin/about/team/780a7413-d421-4a7a-8c51-f45368215490 HTTP/1.1" 500 Internal Server Error
2026-02-01 18:37:23 -06:00: INFO: 127.0.0.1:25648 - "OPTIONS /api/admin/about/team/test HTTP/1.1" 200 OK
2026-02-01 18:37:34 -06:00: INFO: 127.0.0.1:10448 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 18:37:34 -06:00: INFO: 127.0.0.1:10454 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 18:37:34 -06:00: INFO: 127.0.0.1:10448 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 18:37:34 -06:00: INFO: 127.0.0.1:10454 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 18:37:34 -06:00: INFO: 127.0.0.1:10448 - "OPTIONS /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 18:37:34 -06:00: INFO: 127.0.0.1:10454 - "OPTIONS /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 18:37:34 -06:00: INFO: 127.0.0.1:10454 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 18:37:34 -06:00: INFO: 127.0.0.1:10466 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 18:37:36 -06:00: INFO: 127.0.0.1:10454 - "OPTIONS /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 18:37:36 -06:00: INFO: 127.0.0.1:10466 - "OPTIONS /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 18:37:36 -06:00: INFO: 127.0.0.1:10478 - "OPTIONS /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 18:37:36 -06:00: INFO: 127.0.0.1:10454 - "GET /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 18:37:36 -06:00: INFO: 127.0.0.1:10466 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 18:37:36 -06:00: INFO: 127.0.0.1:10484 - "GET /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 18:37:39 -06:00: INFO: 127.0.0.1:10484 - "OPTIONS /api/admin/about/team/780a7413-d421-4a7a-8c51-f45368215490 HTTP/1.1" 200 OK
2026-02-01 18:37:39 -06:00: INFO: 127.0.0.1:10484 - "DELETE /api/admin/about/team/780a7413-d421-4a7a-8c51-f45368215490 HTTP/1.1" 500 Internal Server Error
2026-02-01 18:40:11 -06:00: INFO: 127.0.0.1:47374 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 18:40:11 -06:00: INFO: 127.0.0.1:47388 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 18:40:11 -06:00: INFO: 127.0.0.1:47388 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 18:40:11 -06:00: INFO: 127.0.0.1:47388 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 18:40:11 -06:00: INFO: 127.0.0.1:47392 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 18:40:11 -06:00: INFO: 127.0.0.1:47388 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 18:40:12 -06:00: INFO: 127.0.0.1:47392 - "GET /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 18:40:12 -06:00: INFO: 127.0.0.1:47388 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 18:40:13 -06:00: INFO: 127.0.0.1:47400 - "GET /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 18:40:16 -06:00: INFO: 127.0.0.1:47400 - "DELETE /api/admin/about/team/780a7413-d421-4a7a-8c51-f45368215490 HTTP/1.1" 200 OK
2026-02-01 18:40:16 -06:00: INFO: 127.0.0.1:47400 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 18:40:16 -06:00: INFO: 127.0.0.1:47388 - "GET /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 18:40:16 -06:00: INFO: 127.0.0.1:47392 - "GET /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 18:40:19 -06:00: INFO: 127.0.0.1:47392 - "OPTIONS /api/admin/about/team/27ea484b-d3ce-4f32-9fe0-c17e4f9db685 HTTP/1.1" 200 OK
2026-02-01 18:40:19 -06:00: INFO: 127.0.0.1:47392 - "DELETE /api/admin/about/team/27ea484b-d3ce-4f32-9fe0-c17e4f9db685 HTTP/1.1" 200 OK
2026-02-01 18:40:19 -06:00: INFO: 127.0.0.1:47388 - "GET /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 18:40:19 -06:00: INFO: 127.0.0.1:47392 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 18:40:19 -06:00: INFO: 127.0.0.1:47400 - "GET /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 18:40:21 -06:00: INFO: 127.0.0.1:47392 - "OPTIONS /api/admin/about/team/ac3f6f6d-c190-4108-b4be-567a091f3f6b HTTP/1.1" 200 OK
2026-02-01 18:40:21 -06:00: INFO: 127.0.0.1:47392 - "DELETE /api/admin/about/team/ac3f6f6d-c190-4108-b4be-567a091f3f6b HTTP/1.1" 200 OK
2026-02-01 18:40:21 -06:00: INFO: 127.0.0.1:47392 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 18:40:21 -06:00: INFO: 127.0.0.1:47388 - "GET /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 18:40:21 -06:00: INFO: 127.0.0.1:47400 - "GET /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 18:40:23 -06:00: INFO: 127.0.0.1:47400 - "OPTIONS /api/admin/about/team/c01a0641-8bab-4266-a4c4-f3355c8968aa HTTP/1.1" 200 OK
2026-02-01 18:40:23 -06:00: INFO: 127.0.0.1:47400 - "DELETE /api/admin/about/team/c01a0641-8bab-4266-a4c4-f3355c8968aa HTTP/1.1" 200 OK
2026-02-01 18:40:23 -06:00: INFO: 127.0.0.1:47388 - "GET /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 18:40:23 -06:00: INFO: 127.0.0.1:47392 - "GET /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 18:40:23 -06:00: INFO: 127.0.0.1:47400 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 18:40:30 -06:00: INFO: 127.0.0.1:47412 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 18:40:30 -06:00: INFO: 127.0.0.1:47412 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 18:40:30 -06:00: INFO: None:0 - "GET /uploads/media/aa5bcc15-3b1e-4ed8-8708-1a3dceb9494d.jpg HTTP/1.1" 200 OK
2026-02-01 18:40:31 -06:00: INFO: 127.0.0.1:65534 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 18:40:31 -06:00: INFO: 127.0.0.1:1026 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 18:49:18 -06:00: INFO: 127.0.0.1:62498 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 18:49:18 -06:00: INFO: 127.0.0.1:62492 - "GET /api/about/values HTTP/1.1" 200 OK
2026-02-01 18:49:18 -06:00: INFO: 127.0.0.1:62520 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 18:49:18 -06:00: INFO: 127.0.0.1:62500 - "GET /api/about/team HTTP/1.1" 200 OK
2026-02-01 18:49:18 -06:00: INFO: 127.0.0.1:62492 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 18:49:18 -06:00: INFO: 127.0.0.1:62510 - "GET /api/about/content HTTP/1.1" 200 OK
2026-02-01 18:49:18 -06:00: INFO: 127.0.0.1:62520 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 18:49:27 -06:00: INFO: 127.0.0.1:62524 - "OPTIONS /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 18:49:27 -06:00: INFO: 127.0.0.1:62524 - "OPTIONS /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 18:49:27 -06:00: INFO: 127.0.0.1:24374 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 18:49:27 -06:00: INFO: 127.0.0.1:24354 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 18:49:35 -06:00: INFO: 127.0.0.1:24366 - "GET /api/products HTTP/1.1" 200 OK
2026-02-01 18:49:35 -06:00: INFO: 127.0.0.1:24366 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 18:50:01 -06:00: INFO: 127.0.0.1:47670 - "GET /api/products HTTP/1.1" 200 OK
2026-02-01 18:50:03 -06:00: INFO: 127.0.0.1:47670 - "GET /api/products/d5b1c882-f802-42d9-877b-5fab63156f54 HTTP/1.1" 200 OK
2026-02-01 18:50:17 -06:00: INFO: 127.0.0.1:6902 - "GET /uploads/products/test.jpg HTTP/1.1" 404 Not Found
2026-02-01 18:50:19 -06:00: INFO: 127.0.0.1:6912 - "GET /api/services/9c70d282-8b80-45d0-ba38-e93bd8f0479c HTTP/1.1" 200 OK
2026-02-01 18:50:33 -06:00: INFO: 127.0.0.1:6908 - "GET /uploads/products/test.jpg HTTP/1.1" 404 Not Found
2026-02-01 18:57:06 -06:00: INFO: 127.0.0.1:27250 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 18:57:06 -06:00: INFO: 127.0.0.1:27260 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 18:57:06 -06:00: INFO: 127.0.0.1:27242 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 18:57:06 -06:00: INFO: 127.0.0.1:27260 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 18:57:06 -06:00: INFO: 127.0.0.1:27250 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 18:57:08 -06:00: INFO: 127.0.0.1:27242 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 18:57:08 -06:00: INFO: 127.0.0.1:27260 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 18:57:08 -06:00: INFO: 127.0.0.1:27250 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 18:57:08 -06:00: INFO: 127.0.0.1:27260 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 18:57:08 -06:00: INFO: 127.0.0.1:27242 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 18:57:09 -06:00: INFO: 127.0.0.1:27270 - "GET /uploads/products/test.jpg HTTP/1.1" 404 Not Found
2026-02-01 18:57:49 -06:00: INFO: 127.0.0.1:27266 - "GET /api/services?category=repair HTTP/1.1" 200 OK
2026-02-01 18:57:49 -06:00: INFO: 127.0.0.1:27284 - "GET /uploads/products/test.jpg HTTP/1.1" 404 Not Found
2026-02-01 18:58:45 -06:00: INFO: 127.0.0.1:55632 - "GET /api/about/team HTTP/1.1" 200 OK
2026-02-01 18:58:45 -06:00: INFO: 127.0.0.1:55654 - "GET /api/about/values HTTP/1.1" 200 OK
2026-02-01 18:58:45 -06:00: INFO: 127.0.0.1:55648 - "GET /api/about/content HTTP/1.1" 200 OK
2026-02-01 19:03:30 -06:00: INFO: 127.0.0.1:29532 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 19:03:30 -06:00: INFO: 127.0.0.1:29572 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 19:03:30 -06:00: INFO: 127.0.0.1:29524 - "GET /api/about/team HTTP/1.1" 200 OK
2026-02-01 19:03:30 -06:00: INFO: 127.0.0.1:29544 - "GET /api/about/content HTTP/1.1" 200 OK
2026-02-01 19:03:30 -06:00: INFO: 127.0.0.1:29558 - "GET /api/about/values HTTP/1.1" 200 OK
2026-02-01 19:03:30 -06:00: INFO: 127.0.0.1:29558 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 19:03:30 -06:00: INFO: 127.0.0.1:29572 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 19:03:47 -06:00: INFO: 127.0.0.1:29588 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 19:03:50 -06:00: INFO: 127.0.0.1:29604 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 19:03:50 -06:00: INFO: 127.0.0.1:29604 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 19:03:50 -06:00: INFO: 127.0.0.1:29588 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 19:03:50 -06:00: INFO: 127.0.0.1:29588 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 19:03:50 -06:00: INFO: 127.0.0.1:29588 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 19:03:52 -06:00: INFO: 127.0.0.1:35864 - "GET /uploads/products/test.jpg HTTP/1.1" 404 Not Found
2026-02-01 19:04:51 -06:00: INFO: 127.0.0.1:24658 - "GET /api/products HTTP/1.1" 200 OK
2026-02-01 19:04:52 -06:00: INFO: 127.0.0.1:24658 - "GET /api/about/team HTTP/1.1" 200 OK
2026-02-01 19:04:52 -06:00: INFO: 127.0.0.1:24662 - "GET /api/about/values HTTP/1.1" 200 OK
2026-02-01 19:04:52 -06:00: INFO: 127.0.0.1:24658 - "GET /api/about/content HTTP/1.1" 200 OK
2026-02-01 19:06:00 -06:00: INFO: 127.0.0.1:19770 - "OPTIONS /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 19:06:00 -06:00: INFO: 127.0.0.1:19778 - "OPTIONS /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 19:06:00 -06:00: INFO: 127.0.0.1:19778 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 19:06:00 -06:00: INFO: 127.0.0.1:19770 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 19:06:01 -06:00: INFO: 127.0.0.1:19770 - "OPTIONS /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 19:06:01 -06:00: INFO: 127.0.0.1:19778 - "OPTIONS /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 19:06:01 -06:00: INFO: 127.0.0.1:19782 - "OPTIONS /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 19:06:01 -06:00: INFO: 127.0.0.1:19782 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 19:06:01 -06:00: INFO: 127.0.0.1:19778 - "GET /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 19:06:01 -06:00: INFO: 127.0.0.1:19770 - "GET /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 19:08:51 -06:00: INFO: 127.0.0.1:1378 - "GET /api/about/values HTTP/1.1" 200 OK
2026-02-01 19:08:52 -06:00: INFO: 127.0.0.1:1392 - "GET /api/about/team HTTP/1.1" 200 OK
2026-02-01 19:08:52 -06:00: INFO: 127.0.0.1:1396 - "GET /api/about/content HTTP/1.1" 200 OK
2026-02-01 19:08:54 -06:00: INFO: 127.0.0.1:1398 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 19:08:54 -06:00: INFO: 127.0.0.1:1378 - "GET /api/about/content HTTP/1.1" 200 OK
2026-02-01 19:08:54 -06:00: INFO: 127.0.0.1:1396 - "GET /api/about/team HTTP/1.1" 200 OK
2026-02-01 19:08:54 -06:00: INFO: 127.0.0.1:1392 - "GET /api/about/values HTTP/1.1" 200 OK
2026-02-01 19:08:54 -06:00: INFO: 127.0.0.1:1398 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 19:08:54 -06:00: INFO: None:0 - "GET /uploads/media/aa5bcc15-3b1e-4ed8-8708-1a3dceb9494d.jpg HTTP/1.1" 200 OK
2026-02-01 19:08:54 -06:00: INFO: 127.0.0.1:1392 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 19:08:54 -06:00: INFO: 127.0.0.1:1396 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 19:09:22 -06:00: INFO: 127.0.0.1:1426 - "GET /api/about/team HTTP/1.1" 200 OK
2026-02-01 19:09:22 -06:00: INFO: 127.0.0.1:1418 - "GET /api/about/values HTTP/1.1" 200 OK
2026-02-01 19:09:22 -06:00: INFO: 127.0.0.1:1426 - "GET /api/about/content HTTP/1.1" 200 OK
2026-02-01 19:09:42 -06:00: INFO: 127.0.0.1:49438 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 19:09:42 -06:00: INFO: 127.0.0.1:49438 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 19:09:45 -06:00: INFO: 127.0.0.1:31618 - "GET /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 19:09:45 -06:00: INFO: 127.0.0.1:49438 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 19:09:45 -06:00: INFO: 127.0.0.1:31618 - "GET /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 19:11:32 -06:00: INFO: 127.0.0.1:36300 - "GET /api/about/content HTTP/1.1" 200 OK
2026-02-01 19:11:39 -06:00: INFO: 127.0.0.1:36310 - "GET /api/about/team HTTP/1.1" 200 OK
2026-02-01 19:11:45 -06:00: INFO: 127.0.0.1:25574 - "GET /api/about/values HTTP/1.1" 200 OK
2026-02-01 19:15:14 -06:00: INFO: 127.0.0.1:63146 - "GET /api/about/content HTTP/1.1" 200 OK
2026-02-01 19:15:39 -06:00: INFO: 127.0.0.1:49584 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 19:15:39 -06:00: INFO: 127.0.0.1:49596 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 19:15:40 -06:00: INFO: 127.0.0.1:49596 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 19:15:40 -06:00: INFO: 127.0.0.1:49596 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 19:15:40 -06:00: INFO: 127.0.0.1:49596 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 19:15:40 -06:00: INFO: 127.0.0.1:49610 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 19:15:42 -06:00: INFO: 127.0.0.1:49610 - "GET /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 19:15:42 -06:00: INFO: 127.0.0.1:49596 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 19:15:42 -06:00: INFO: 127.0.0.1:49614 - "GET /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 19:16:00 -06:00: INFO: 127.0.0.1:49628 - "GET /api/about/team HTTP/1.1" 200 OK
2026-02-01 19:16:00 -06:00: INFO: 127.0.0.1:49638 - "GET /api/about/values HTTP/1.1" 200 OK
2026-02-01 19:16:00 -06:00: INFO: 127.0.0.1:49628 - "GET /api/about/content HTTP/1.1" 200 OK
2026-02-01 19:16:02 -06:00: INFO: 127.0.0.1:49628 - "GET /api/about/team HTTP/1.1" 200 OK
2026-02-01 19:16:02 -06:00: INFO: 127.0.0.1:49638 - "GET /api/about/values HTTP/1.1" 200 OK
2026-02-01 19:16:02 -06:00: INFO: 127.0.0.1:45578 - "GET /api/about/content HTTP/1.1" 200 OK
2026-02-01 19:16:02 -06:00: INFO: 127.0.0.1:49628 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 19:16:02 -06:00: INFO: 127.0.0.1:49638 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 19:16:02 -06:00: INFO: None:0 - "GET /uploads/media/aa5bcc15-3b1e-4ed8-8708-1a3dceb9494d.jpg HTTP/1.1" 200 OK
2026-02-01 19:16:02 -06:00: INFO: 127.0.0.1:45578 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 19:16:02 -06:00: INFO: 127.0.0.1:45578 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 19:16:24 -06:00: INFO: 127.0.0.1:14394 - "OPTIONS /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 19:16:24 -06:00: INFO: 127.0.0.1:14388 - "OPTIONS /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 19:16:24 -06:00: INFO: 127.0.0.1:14394 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 19:16:24 -06:00: INFO: 127.0.0.1:14388 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 19:16:27 -06:00: INFO: 127.0.0.1:14394 - "OPTIONS /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 19:16:27 -06:00: INFO: 127.0.0.1:14388 - "OPTIONS /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 19:16:27 -06:00: INFO: 127.0.0.1:14408 - "OPTIONS /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 19:16:27 -06:00: INFO: 127.0.0.1:14408 - "GET /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 19:16:27 -06:00: INFO: 127.0.0.1:14394 - "GET /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 19:16:27 -06:00: INFO: 127.0.0.1:14388 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 19:18:24 -06:00: INFO: 127.0.0.1:16868 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 19:18:24 -06:00: INFO: 127.0.0.1:16870 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 19:21:44 -06:00: INFO: 127.0.0.1:65258 - "POST /api/token HTTP/1.1" 404 Not Found
2026-02-01 19:21:44 -06:00: INFO: 127.0.0.1:65274 - "GET /api/admin/about/content HTTP/1.1" 401 Unauthorized
2026-02-01 19:21:55 -06:00: INFO: 127.0.0.1:51116 - "POST /api/token HTTP/1.1" 404 Not Found
2026-02-01 19:21:55 -06:00: INFO: 127.0.0.1:51118 - "GET /api/admin/about/content HTTP/1.1" 401 Unauthorized
2026-02-01 19:22:06 -06:00: INFO: 127.0.0.1:24376 - "POST /api/token HTTP/1.1" 404 Not Found
2026-02-01 19:22:06 -06:00: INFO: 127.0.0.1:24384 - "GET /api/admin/about/content HTTP/1.1" 401 Unauthorized
2026-02-01 19:22:13 -06:00: INFO: 127.0.0.1:19636 - "POST /api/token HTTP/1.1" 404 Not Found
2026-02-01 19:22:13 -06:00: INFO: 127.0.0.1:19652 - "GET /api/admin/about/content HTTP/1.1" 401 Unauthorized
2026-02-01 19:22:41 -06:00: INFO: 127.0.0.1:64748 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 19:22:41 -06:00: INFO: 127.0.0.1:64756 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 19:22:58 -06:00: INFO: 127.0.0.1:16298 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 19:22:58 -06:00: INFO: 127.0.0.1:16308 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 19:22:58 -06:00: INFO: 127.0.0.1:16308 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 19:22:58 -06:00: INFO: 127.0.0.1:16308 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 19:22:58 -06:00: INFO: 127.0.0.1:16308 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 19:22:58 -06:00: INFO: 127.0.0.1:16314 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 19:22:59 -06:00: INFO: 127.0.0.1:16308 - "GET /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 19:22:59 -06:00: INFO: 127.0.0.1:16314 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 19:22:59 -06:00: INFO: 127.0.0.1:16320 - "GET /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 19:23:23 -06:00: INFO: 127.0.0.1:16328 - "OPTIONS /api/media?page=1&limit=24 HTTP/1.1" 200 OK
2026-02-01 19:23:24 -06:00: INFO: 127.0.0.1:16328 - "GET /api/media?page=1&limit=24 HTTP/1.1" 200 OK
2026-02-01 19:23:24 -06:00: INFO: 127.0.0.1:28126 - "GET /uploads/media/aa5bcc15-3b1e-4ed8-8708-1a3dceb9494d.jpg HTTP/1.1" 304 Not Modified
2026-02-01 19:23:26 -06:00: INFO: 127.0.0.1:16328 - "OPTIONS /api/admin/reports/sales?period=monthly HTTP/1.1" 200 OK
2026-02-01 19:23:26 -06:00: INFO: 127.0.0.1:16328 - "GET /api/admin/reports/sales?period=monthly HTTP/1.1" 200 OK
2026-02-01 19:23:31 -06:00: INFO: 127.0.0.1:16342 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 19:23:31 -06:00: INFO: 127.0.0.1:16328 - "GET /api/products HTTP/1.1" 200 OK
2026-02-01 19:24:27 -06:00: INFO: 127.0.0.1:46252 - "OPTIONS /api/orders HTTP/1.1" 200 OK
2026-02-01 19:24:27 -06:00: INFO: 127.0.0.1:46252 - "GET /api/orders HTTP/1.1" 200 OK
2026-02-01 19:24:30 -06:00: INFO: 127.0.0.1:46252 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 19:24:32 -06:00: INFO: 127.0.0.1:58148 - "GET /uploads/products/test.jpg HTTP/1.1" 404 Not Found
2026-02-01 19:24:40 -06:00: INFO: 127.0.0.1:46266 - "GET /api/about/team HTTP/1.1" 200 OK
2026-02-01 19:24:40 -06:00: INFO: 127.0.0.1:46266 - "GET /api/about/values HTTP/1.1" 200 OK
2026-02-01 19:24:40 -06:00: INFO: 127.0.0.1:46266 - "GET /api/about/content HTTP/1.1" 200 OK
2026-02-01 19:28:42 -06:00: INFO: 127.0.0.1:62306 - "OPTIONS /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 19:28:42 -06:00: INFO: 127.0.0.1:62316 - "OPTIONS /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 19:28:42 -06:00: INFO: 127.0.0.1:62306 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 19:28:42 -06:00: INFO: 127.0.0.1:62316 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 19:28:44 -06:00: INFO: 127.0.0.1:62316 - "OPTIONS /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 19:28:44 -06:00: INFO: 127.0.0.1:62306 - "OPTIONS /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 19:28:44 -06:00: INFO: 127.0.0.1:62318 - "OPTIONS /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 19:28:44 -06:00: INFO: 127.0.0.1:62316 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 19:28:44 -06:00: INFO: 127.0.0.1:62318 - "GET /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 19:28:44 -06:00: INFO: 127.0.0.1:62306 - "GET /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 19:28:59 -06:00: INFO: 127.0.0.1:7890 - "OPTIONS /api/admin/about/content/71f29e56-a68b-4114-a931-e8fd08a787df HTTP/1.1" 200 OK
2026-02-01 19:28:59 -06:00: INFO: 127.0.0.1:7890 - "PUT /api/admin/about/content/71f29e56-a68b-4114-a931-e8fd08a787df HTTP/1.1" 500 Internal Server Error
2026-02-01 19:35:40 -06:00: INFO: 127.0.0.1:62240 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 19:35:40 -06:00: INFO: 127.0.0.1:62246 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 19:35:40 -06:00: INFO: 127.0.0.1:62260 - "PUT /api/admin/about/content/71f29e56-a68b-4114-a931-e8fd08a787df HTTP/1.1" 500 Internal Server Error
2026-02-01 19:35:54 -06:00: INFO: 127.0.0.1:42960 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 19:35:54 -06:00: INFO: 127.0.0.1:42970 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 19:35:54 -06:00: INFO: 127.0.0.1:42978 - "PUT /api/admin/about/content/71f29e56-a68b-4114-a931-e8fd08a787df HTTP/1.1" 500 Internal Server Error
2026-02-01 19:36:13 -06:00: INFO: 127.0.0.1:19682 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 19:36:13 -06:00: INFO: 127.0.0.1:19684 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 19:36:13 -06:00: INFO: 127.0.0.1:19692 - "PUT /api/admin/about/content/71f29e56-a68b-4114-a931-e8fd08a787df HTTP/1.1" 500 Internal Server Error
2026-02-01 19:39:46 -06:00: INFO: 127.0.0.1:28416 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 19:39:46 -06:00: INFO: 127.0.0.1:28432 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 19:39:46 -06:00: INFO: 127.0.0.1:28440 - "PUT /api/admin/about/content/71f29e56-a68b-4114-a931-e8fd08a787df HTTP/1.1" 200 OK
2026-02-01 19:40:10 -06:00: INFO: 127.0.0.1:60304 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 19:40:10 -06:00: INFO: 127.0.0.1:60320 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 19:40:10 -06:00: INFO: 127.0.0.1:60326 - "PUT /api/admin/about/content/71f29e56-a68b-4114-a931-e8fd08a787df HTTP/1.1" 200 OK
2026-02-01 19:40:44 -06:00: INFO: 127.0.0.1:34302 - "GET /api/about/content HTTP/1.1" 200 OK
2026-02-01 19:40:54 -06:00: INFO: 127.0.0.1:63638 - "GET /api/about/content HTTP/1.1" 200 OK
2026-02-01 19:41:26 -06:00: INFO: 127.0.0.1:11612 - "GET /api/about/content HTTP/1.1" 200 OK
2026-02-01 19:50:36 -06:00: INFO: 127.0.0.1:21932 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 19:50:36 -06:00: INFO: 127.0.0.1:21938 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 19:50:36 -06:00: INFO: 127.0.0.1:21938 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 19:50:36 -06:00: INFO: 127.0.0.1:21938 - "OPTIONS /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 19:50:36 -06:00: INFO: 127.0.0.1:21938 - "OPTIONS /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 19:50:36 -06:00: INFO: 127.0.0.1:21932 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 19:50:36 -06:00: INFO: 127.0.0.1:21932 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 19:50:36 -06:00: INFO: 127.0.0.1:21932 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 19:50:37 -06:00: INFO: 127.0.0.1:21952 - "OPTIONS /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 19:50:37 -06:00: INFO: 127.0.0.1:21932 - "OPTIONS /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 19:50:37 -06:00: INFO: 127.0.0.1:21958 - "OPTIONS /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 19:50:37 -06:00: INFO: 127.0.0.1:21966 - "GET /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 19:50:37 -06:00: INFO: 127.0.0.1:21932 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 19:50:37 -06:00: INFO: 127.0.0.1:21958 - "GET /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 19:50:47 -06:00: INFO: 127.0.0.1:21976 - "OPTIONS /api/admin/about/content/71f29e56-a68b-4114-a931-e8fd08a787df HTTP/1.1" 200 OK
2026-02-01 19:50:47 -06:00: INFO: 127.0.0.1:21976 - "PUT /api/admin/about/content/71f29e56-a68b-4114-a931-e8fd08a787df HTTP/1.1" 200 OK
2026-02-01 19:50:47 -06:00: INFO: 127.0.0.1:21976 - "GET /api/admin/about/content HTTP/1.1" 200 OK
2026-02-01 19:50:47 -06:00: INFO: 127.0.0.1:21976 - "GET /api/admin/about/team HTTP/1.1" 200 OK
2026-02-01 19:50:47 -06:00: INFO: 127.0.0.1:21976 - "GET /api/admin/about/values HTTP/1.1" 200 OK
2026-02-01 19:50:49 -06:00: INFO: 127.0.0.1:21976 - "GET /api/about/team HTTP/1.1" 200 OK
2026-02-01 19:50:49 -06:00: INFO: 127.0.0.1:1254 - "GET /api/about/values HTTP/1.1" 200 OK
2026-02-01 19:50:49 -06:00: INFO: 127.0.0.1:1246 - "GET /api/about/content HTTP/1.1" 200 OK
2026-02-01 19:51:41 -06:00: INFO: 127.0.0.1:26774 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 19:51:44 -06:00: INFO: 127.0.0.1:11628 - "GET /uploads/products/test.jpg HTTP/1.1" 404 Not Found
2026-02-01 19:55:04 -06:00: INFO: 127.0.0.1:13980 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 19:55:04 -06:00: INFO: 127.0.0.1:13994 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 19:55:04 -06:00: INFO: 127.0.0.1:13994 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 19:55:04 -06:00: INFO: 127.0.0.1:13980 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 19:55:04 -06:00: INFO: 127.0.0.1:13998 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 19:55:07 -06:00: INFO: 127.0.0.1:13994 - "GET /api/products HTTP/1.1" 200 OK
2026-02-01 19:55:08 -06:00: INFO: 127.0.0.1:13994 - "GET /api/products HTTP/1.1" 200 OK
2026-02-01 19:55:11 -06:00: INFO: 127.0.0.1:14004 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 19:55:11 -06:00: INFO: 127.0.0.1:13994 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 19:55:13 -06:00: INFO: 127.0.0.1:13994 - "OPTIONS /api/admin/products?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 19:55:13 -06:00: INFO: 127.0.0.1:13994 - "GET /api/admin/products?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 19:55:46 -06:00: INFO: 127.0.0.1:46564 - "GET /api/products/1dd042e3-963d-430f-89c2-1eee235b345a HTTP/1.1" 200 OK
2026-02-01 19:56:07 -06:00: INFO: 127.0.0.1:46566 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 19:56:07 -06:00: INFO: 127.0.0.1:46566 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 19:56:09 -06:00: INFO: 127.0.0.1:46566 - "OPTIONS /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 19:56:09 -06:00: INFO: 127.0.0.1:46566 - "GET /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 19:56:09 -06:00: INFO: 127.0.0.1:9634 - "GET /uploads/products/test.jpg HTTP/1.1" 404 Not Found
2026-02-01 19:56:10 -06:00: INFO: 127.0.0.1:46566 - "OPTIONS /api/admin/orders HTTP/1.1" 200 OK
2026-02-01 19:56:10 -06:00: INFO: 127.0.0.1:46566 - "GET /api/admin/orders HTTP/1.1" 200 OK
2026-02-01 19:56:11 -06:00: INFO: 127.0.0.1:9650 - "GET /uploads/products/test.jpg HTTP/1.1" 404 Not Found
2026-02-01 19:56:11 -06:00: INFO: 127.0.0.1:46566 - "GET /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 19:56:48 -06:00: INFO: 127.0.0.1:9626 - "OPTIONS /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 19:56:48 -06:00: INFO: 127.0.0.1:9626 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 19:57:53 -06:00: INFO: 127.0.0.1:9394 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 19:57:53 -06:00: INFO: 127.0.0.1:9398 - "POST /api/admin/products HTTP/1.1" 422 Unprocessable Entity
2026-02-01 19:58:14 -06:00: INFO: 127.0.0.1:49552 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 19:58:14 -06:00: INFO: 127.0.0.1:49554 - "POST /api/admin/products HTTP/1.1" 200 OK
2026-02-01 19:58:48 -06:00: INFO: 127.0.0.1:19776 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 19:58:48 -06:00: INFO: 127.0.0.1:19780 - "PUT /api/admin/products/05d789f1-09f4-42f6-9315-33683833c96d HTTP/1.1" 200 OK
2026-02-01 19:59:00 -06:00: INFO: 127.0.0.1:35170 - "GET /api/products HTTP/1.1" 200 OK
2026-02-01 20:01:04 -06:00: INFO: 127.0.0.1:22834 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 20:01:04 -06:00: INFO: 127.0.0.1:22850 - "DELETE /api/admin/products/05d789f1-09f4-42f6-9315-33683833c96d HTTP/1.1" 200 OK
2026-02-01 20:01:51 -06:00: INFO: 127.0.0.1:26872 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 20:01:51 -06:00: INFO: 127.0.0.1:26876 - "GET /api/admin/services HTTP/1.1" 200 OK
2026-02-01 20:02:28 -06:00: INFO: 127.0.0.1:11592 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 20:02:28 -06:00: INFO: 127.0.0.1:11608 - "POST /api/admin/services HTTP/1.1" 200 OK
2026-02-01 20:03:18 -06:00: INFO: 127.0.0.1:40692 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 20:03:18 -06:00: INFO: 127.0.0.1:40698 - "PUT /api/admin/services/59459c6d-ff72-49a0-bba4-a4966ffa84e0 HTTP/1.1" 200 OK
2026-02-01 20:03:33 -06:00: INFO: 127.0.0.1:43930 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 20:03:46 -06:00: INFO: 127.0.0.1:58864 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 20:03:46 -06:00: INFO: 127.0.0.1:58874 - "DELETE /api/admin/services/59459c6d-ff72-49a0-bba4-a4966ffa84e0 HTTP/1.1" 200 OK
2026-02-01 20:07:47 -06:00: INFO: 127.0.0.1:30124 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 20:07:47 -06:00: INFO: 127.0.0.1:30130 - "GET /api/admin/inventory HTTP/1.1" 200 OK
2026-02-01 20:08:04 -06:00: INFO: 127.0.0.1:22338 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 20:08:04 -06:00: INFO: 127.0.0.1:22350 - "GET /api/admin/inventory HTTP/1.1" 200 OK
2026-02-01 20:08:45 -06:00: INFO: 127.0.0.1:22152 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 20:08:45 -06:00: INFO: 127.0.0.1:22160 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 20:09:01 -06:00: INFO: 127.0.0.1:13828 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 20:09:01 -06:00: INFO: 127.0.0.1:13844 - "POST /api/services/book HTTP/1.1" 200 OK
2026-02-01 20:09:12 -06:00: INFO: 127.0.0.1:27660 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 20:09:12 -06:00: INFO: 127.0.0.1:27664 - "POST /api/services/book HTTP/1.1" 200 OK
2026-02-01 20:09:41 -06:00: INFO: 127.0.0.1:24388 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 20:09:41 -06:00: INFO: 127.0.0.1:20202 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 20:21:04 -06:00: INFO: 127.0.0.1:33546 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 20:21:04 -06:00: INFO: 127.0.0.1:33558 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 20:21:04 -06:00: INFO: 127.0.0.1:33558 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 20:21:04 -06:00: INFO: 127.0.0.1:33558 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 20:21:04 -06:00: INFO: 127.0.0.1:33558 - "OPTIONS /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 20:21:04 -06:00: INFO: 127.0.0.1:33558 - "OPTIONS /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 20:21:04 -06:00: INFO: 127.0.0.1:33572 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 20:21:04 -06:00: INFO: 127.0.0.1:33572 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 20:21:06 -06:00: INFO: 127.0.0.1:33572 - "OPTIONS /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 20:21:06 -06:00: INFO: 127.0.0.1:33572 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 20:21:17 -06:00: INFO: 127.0.0.1:33588 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 20:21:18 -06:00: INFO: 127.0.0.1:18068 - "GET /uploads/products/test.jpg HTTP/1.1" 404 Not Found
2026-02-01 20:21:35 -06:00: INFO: 127.0.0.1:33598 - "GET /api/services/9c70d282-8b80-45d0-ba38-e93bd8f0479c HTTP/1.1" 200 OK
2026-02-01 20:22:41 -06:00: INFO: 127.0.0.1:33632 - "OPTIONS /api/services/book HTTP/1.1" 200 OK
2026-02-01 20:22:41 -06:00: INFO: 127.0.0.1:33632 - "POST /api/services/book HTTP/1.1" 200 OK
2026-02-01 20:22:52 -06:00: INFO: 127.0.0.1:33642 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 20:22:52 -06:00: INFO: 127.0.0.1:33642 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 20:22:54 -06:00: INFO: 127.0.0.1:33642 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 20:23:03 -06:00: INFO: 127.0.0.1:42694 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 20:23:06 -06:00: INFO: 127.0.0.1:42694 - "OPTIONS /api/admin/users?skip=0&limit=20&search=&role=&status= HTTP/1.1" 200 OK
2026-02-01 20:23:06 -06:00: INFO: 127.0.0.1:42694 - "GET /api/admin/users?skip=0&limit=20&search=&role=&status= HTTP/1.1" 200 OK
2026-02-01 20:23:08 -06:00: INFO: 127.0.0.1:42694 - "OPTIONS /api/admin/reports/sales?period=monthly HTTP/1.1" 200 OK
2026-02-01 20:23:08 -06:00: INFO: 127.0.0.1:42694 - "GET /api/admin/reports/sales?period=monthly HTTP/1.1" 200 OK
2026-02-01 20:23:19 -06:00: INFO: 127.0.0.1:50722 - "OPTIONS /api/admin/products?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 20:23:19 -06:00: INFO: 127.0.0.1:50722 - "GET /api/admin/products?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 20:23:27 -06:00: INFO: 127.0.0.1:50732 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 21:14:23 -06:00: INFO: 127.0.0.1:52242 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 21:14:23 -06:00: INFO: 127.0.0.1:52248 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 21:14:23 -06:00: INFO: 127.0.0.1:52256 - "PUT /api/admin/bookings/8c6db590-becc-479f-a316-831973c584db/complete HTTP/1.1" 200 OK
2026-02-01 21:14:33 -06:00: INFO: 127.0.0.1:34040 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 21:14:33 -06:00: INFO: 127.0.0.1:34050 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 21:14:33 -06:00: INFO: 127.0.0.1:34062 - "GET /api/admin/bookings/8c6db590-becc-479f-a316-831973c584db/receipt HTTP/1.1" 200 OK
2026-02-01 21:14:54 -06:00: INFO: 127.0.0.1:5066 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 21:14:54 -06:00: INFO: 127.0.0.1:5068 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 21:14:54 -06:00: INFO: 127.0.0.1:5066 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 21:14:54 -06:00: INFO: 127.0.0.1:5068 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 21:14:54 -06:00: INFO: 127.0.0.1:5068 - "OPTIONS /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 21:14:54 -06:00: INFO: 127.0.0.1:5066 - "OPTIONS /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 21:14:54 -06:00: INFO: 127.0.0.1:5066 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 21:14:54 -06:00: INFO: 127.0.0.1:5084 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 21:14:56 -06:00: INFO: 127.0.0.1:5084 - "OPTIONS /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 21:14:56 -06:00: INFO: 127.0.0.1:5084 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 21:15:07 -06:00: INFO: 127.0.0.1:5090 - "OPTIONS /api/admin/bookings/8c6db590-becc-479f-a316-831973c584db/receipt HTTP/1.1" 200 OK
2026-02-01 21:15:07 -06:00: INFO: 127.0.0.1:5090 - "GET /api/admin/bookings/8c6db590-becc-479f-a316-831973c584db/receipt HTTP/1.1" 200 OK
2026-02-01 21:16:34 -06:00: INFO: 127.0.0.1:31348 - "GET /api/admin/bookings/8c6db590-becc-479f-a316-831973c584db/receipt HTTP/1.1" 200 OK
2026-02-01 21:22:56 -06:00: INFO: 127.0.0.1:54392 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 21:22:56 -06:00: INFO: 127.0.0.1:54400 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 21:22:56 -06:00: INFO: 127.0.0.1:54392 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 21:22:56 -06:00: INFO: 127.0.0.1:54400 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 21:22:56 -06:00: INFO: 127.0.0.1:54392 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 21:22:56 -06:00: INFO: 127.0.0.1:54400 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 21:22:59 -06:00: INFO: 127.0.0.1:54400 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 21:23:04 -06:00: INFO: 127.0.0.1:54400 - "GET /api/admin/bookings/8c6db590-becc-479f-a316-831973c584db/receipt HTTP/1.1" 200 OK
2026-02-01 21:25:26 -06:00: INFO: 127.0.0.1:62462 - "OPTIONS /api/admin/bookings/703b913e-20d7-4356-99f0-b33b8b28a89c/status?status=in-progress HTTP/1.1" 200 OK
2026-02-01 21:25:26 -06:00: INFO: 127.0.0.1:62462 - "PUT /api/admin/bookings/703b913e-20d7-4356-99f0-b33b8b28a89c/status?status=in-progress HTTP/1.1" 200 OK
2026-02-01 21:25:26 -06:00: INFO: 127.0.0.1:62462 - "OPTIONS /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 21:25:26 -06:00: INFO: 127.0.0.1:62476 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 21:25:44 -06:00: INFO: 127.0.0.1:57300 - "OPTIONS /api/admin/reports/sales?period=monthly HTTP/1.1" 200 OK
2026-02-01 21:25:44 -06:00: INFO: 127.0.0.1:57300 - "GET /api/admin/reports/sales?period=monthly HTTP/1.1" 200 OK
2026-02-01 21:25:50 -06:00: INFO: 127.0.0.1:57310 - "OPTIONS /api/admin/inventory HTTP/1.1" 200 OK
2026-02-01 21:25:50 -06:00: INFO: 127.0.0.1:57310 - "GET /api/admin/inventory HTTP/1.1" 200 OK
2026-02-01 21:25:54 -06:00: INFO: 127.0.0.1:57310 - "OPTIONS /api/admin/orders HTTP/1.1" 200 OK
2026-02-01 21:25:55 -06:00: INFO: 127.0.0.1:8022 - "GET /api/admin/orders HTTP/1.1" 200 OK
2026-02-01 21:25:55 -06:00: INFO: 127.0.0.1:8022 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 21:25:56 -06:00: INFO: 127.0.0.1:8022 - "OPTIONS /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 21:25:56 -06:00: INFO: 127.0.0.1:8022 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 21:25:57 -06:00: INFO: 127.0.0.1:8022 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 21:27:02 -06:00: INFO: 127.0.0.1:60670 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 21:27:02 -06:00: INFO: 127.0.0.1:60672 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 21:27:03 -06:00: INFO: 127.0.0.1:60670 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 21:27:03 -06:00: INFO: 127.0.0.1:60672 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 21:27:03 -06:00: INFO: 127.0.0.1:60672 - "OPTIONS /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 21:27:03 -06:00: INFO: 127.0.0.1:60670 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 21:27:03 -06:00: INFO: 127.0.0.1:60670 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 21:27:09 -06:00: INFO: 127.0.0.1:60680 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 21:27:44 -06:00: INFO: 127.0.0.1:60696 - "OPTIONS /api/admin/bookings/8c6db590-becc-479f-a316-831973c584db/receipt HTTP/1.1" 200 OK
2026-02-01 21:27:44 -06:00: INFO: 127.0.0.1:60696 - "GET /api/admin/bookings/8c6db590-becc-479f-a316-831973c584db/receipt HTTP/1.1" 200 OK
2026-02-01 21:28:49 -06:00: INFO: 127.0.0.1:20266 - "GET /api/admin/bookings/8c6db590-becc-479f-a316-831973c584db/receipt HTTP/1.1" 200 OK
2026-02-01 21:31:01 -06:00: INFO: 127.0.0.1:61196 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 21:31:01 -06:00: INFO: 127.0.0.1:61210 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 21:31:01 -06:00: INFO: 127.0.0.1:61196 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 21:31:01 -06:00: INFO: 127.0.0.1:61210 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 21:31:01 -06:00: INFO: 127.0.0.1:61210 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 21:31:01 -06:00: INFO: 127.0.0.1:61196 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 21:31:03 -06:00: INFO: 127.0.0.1:61196 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 21:31:10 -06:00: INFO: 127.0.0.1:61216 - "OPTIONS /api/admin/bookings/703b913e-20d7-4356-99f0-b33b8b28a89c HTTP/1.1" 200 OK
2026-02-01 21:31:10 -06:00: INFO: 127.0.0.1:61216 - "DELETE /api/admin/bookings/703b913e-20d7-4356-99f0-b33b8b28a89c HTTP/1.1" 200 OK
2026-02-01 21:31:10 -06:00: INFO: 127.0.0.1:61216 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 21:32:59 -06:00: INFO: 127.0.0.1:23826 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 21:32:59 -06:00: INFO: 127.0.0.1:23840 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 21:32:59 -06:00: INFO: 127.0.0.1:23840 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 21:32:59 -06:00: INFO: 127.0.0.1:23826 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 21:32:59 -06:00: INFO: 127.0.0.1:23840 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 21:32:59 -06:00: INFO: 127.0.0.1:23826 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 21:33:01 -06:00: INFO: 127.0.0.1:23842 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 21:33:03 -06:00: INFO: 127.0.0.1:23842 - "GET /api/admin/bookings/8c6db590-becc-479f-a316-831973c584db/receipt HTTP/1.1" 200 OK
2026-02-01 21:35:00 -06:00: INFO: 127.0.0.1:57440 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 21:35:01 -06:00: INFO: 127.0.0.1:57472 - "GET /uploads/products/test.jpg HTTP/1.1" 404 Not Found
2026-02-01 21:35:02 -06:00: INFO: 127.0.0.1:57440 - "GET /api/services/e9fdd79e-7728-4ca5-a961-b9aa2be10124 HTTP/1.1" 200 OK
2026-02-01 21:35:33 -06:00: INFO: 127.0.0.1:57456 - "GET /api/services/e9fdd79e-7728-4ca5-a961-b9aa2be10124 HTTP/1.1" 200 OK
2026-02-01 21:35:33 -06:00: INFO: 127.0.0.1:57456 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 21:35:33 -06:00: INFO: 127.0.0.1:2446 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 21:35:34 -06:00: INFO: 127.0.0.1:2454 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 21:35:34 -06:00: INFO: 127.0.0.1:2446 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 21:35:35 -06:00: INFO: 127.0.0.1:2454 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 21:35:35 -06:00: INFO: 127.0.0.1:2446 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 21:35:39 -06:00: INFO: 127.0.0.1:2446 - "OPTIONS /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 21:35:39 -06:00: INFO: 127.0.0.1:2446 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 21:35:40 -06:00: INFO: 127.0.0.1:2446 - "GET /api/admin/bookings/8c6db590-becc-479f-a316-831973c584db/receipt HTTP/1.1" 200 OK
2026-02-01 21:37:59 -06:00: INFO: 127.0.0.1:25062 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 21:37:59 -06:00: INFO: 127.0.0.1:25072 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 21:37:59 -06:00: INFO: 127.0.0.1:25062 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 21:37:59 -06:00: INFO: 127.0.0.1:25072 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 21:37:59 -06:00: INFO: 127.0.0.1:25072 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 21:37:59 -06:00: INFO: 127.0.0.1:25062 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 21:37:59 -06:00: INFO: 127.0.0.1:25062 - "OPTIONS /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 21:37:59 -06:00: INFO: 127.0.0.1:25072 - "OPTIONS /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 21:37:59 -06:00: INFO: 127.0.0.1:25062 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 21:37:59 -06:00: INFO: 127.0.0.1:25072 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 21:38:01 -06:00: INFO: 127.0.0.1:25072 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 21:38:02 -06:00: INFO: 127.0.0.1:25072 - "OPTIONS /api/admin/bookings/8c6db590-becc-479f-a316-831973c584db/receipt HTTP/1.1" 200 OK
2026-02-01 21:38:02 -06:00: INFO: 127.0.0.1:25072 - "GET /api/admin/bookings/8c6db590-becc-479f-a316-831973c584db/receipt HTTP/1.1" 200 OK
2026-02-01 21:39:16 -06:00: INFO: 127.0.0.1:58864 - "OPTIONS /api/admin/bookings/8c6db590-becc-479f-a316-831973c584db HTTP/1.1" 200 OK
2026-02-01 21:39:16 -06:00: INFO: 127.0.0.1:58864 - "DELETE /api/admin/bookings/8c6db590-becc-479f-a316-831973c584db HTTP/1.1" 200 OK
2026-02-01 21:39:16 -06:00: INFO: 127.0.0.1:58864 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 21:39:18 -06:00: INFO: 127.0.0.1:58864 - "OPTIONS /api/admin/bookings/2ca83d28-a948-4365-a784-7f6b834f1cc3 HTTP/1.1" 200 OK
2026-02-01 21:39:18 -06:00: INFO: 127.0.0.1:58864 - "DELETE /api/admin/bookings/2ca83d28-a948-4365-a784-7f6b834f1cc3 HTTP/1.1" 200 OK
2026-02-01 21:39:18 -06:00: INFO: 127.0.0.1:58864 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 21:39:20 -06:00: INFO: 127.0.0.1:58864 - "OPTIONS /api/admin/reports/sales?period=monthly HTTP/1.1" 200 OK
2026-02-01 21:39:20 -06:00: INFO: 127.0.0.1:58864 - "GET /api/admin/reports/sales?period=monthly HTTP/1.1" 200 OK
2026-02-01 21:41:34 -06:00: INFO: 127.0.0.1:42058 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 21:41:34 -06:00: INFO: 127.0.0.1:42058 - "GET /api/products HTTP/1.1" 200 OK
2026-02-01 21:41:36 -06:00: INFO: 127.0.0.1:42066 - "GET /uploads/products/test.jpg HTTP/1.1" 404 Not Found
2026-02-01 21:41:37 -06:00: INFO: 127.0.0.1:42058 - "GET /api/services/e9fdd79e-7728-4ca5-a961-b9aa2be10124 HTTP/1.1" 200 OK
2026-02-01 21:41:50 -06:00: INFO: 127.0.0.1:5684 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 21:41:50 -06:00: INFO: 127.0.0.1:5676 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 21:42:19 -06:00: INFO: 127.0.0.1:57242 - "OPTIONS /api/admin/inventory HTTP/1.1" 200 OK
2026-02-01 21:42:19 -06:00: INFO: 127.0.0.1:57242 - "GET /api/admin/inventory HTTP/1.1" 200 OK
2026-02-01 21:46:21 -06:00: INFO: 127.0.0.1:52242 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 21:46:21 -06:00: INFO: 127.0.0.1:52248 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 21:46:21 -06:00: INFO: 127.0.0.1:52242 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 21:46:21 -06:00: INFO: 127.0.0.1:52248 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 21:46:22 -06:00: INFO: 127.0.0.1:52242 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 21:46:22 -06:00: INFO: 127.0.0.1:52248 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 21:46:22 -06:00: INFO: 127.0.0.1:52248 - "OPTIONS /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 21:46:23 -06:00: INFO: 127.0.0.1:52248 - "GET /api/admin/bookings HTTP/1.1" 200 OK
2026-02-01 21:48:12 -06:00: INFO: 127.0.0.1:32150 - "OPTIONS /api/media?page=1&limit=24 HTTP/1.1" 200 OK
2026-02-01 21:48:12 -06:00: INFO: 127.0.0.1:32150 - "GET /api/media?page=1&limit=24 HTTP/1.1" 200 OK
2026-02-01 21:48:12 -06:00: INFO: 127.0.0.1:32168 - "GET /uploads/media/aa5bcc15-3b1e-4ed8-8708-1a3dceb9494d.jpg HTTP/1.1" 304 Not Modified
2026-02-01 21:48:21 -06:00: INFO: 127.0.0.1:18696 - "OPTIONS /api/admin/products?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 21:48:21 -06:00: INFO: 127.0.0.1:18696 - "GET /api/admin/products?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 21:48:58 -06:00: INFO: 127.0.0.1:53088 - "OPTIONS /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 21:48:59 -06:00: INFO: 127.0.0.1:53098 - "GET /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 21:48:59 -06:00: INFO: 127.0.0.1:53122 - "GET /uploads/products/test.jpg HTTP/1.1" 404 Not Found
2026-02-01 21:49:09 -06:00: INFO: 127.0.0.1:53110 - "OPTIONS /api/admin/services/7a2cd09a-e8fe-4360-b006-aeafa1c23fa5 HTTP/1.1" 200 OK
2026-02-01 21:49:09 -06:00: INFO: 127.0.0.1:53110 - "DELETE /api/admin/services/7a2cd09a-e8fe-4360-b006-aeafa1c23fa5 HTTP/1.1" 200 OK
2026-02-01 21:49:09 -06:00: INFO: 127.0.0.1:53110 - "GET /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 21:49:13 -06:00: INFO: 127.0.0.1:53110 - "OPTIONS /api/admin/services/528d18a7-e24f-432e-bf1d-842f534c44f0 HTTP/1.1" 200 OK
2026-02-01 21:49:13 -06:00: INFO: 127.0.0.1:53110 - "DELETE /api/admin/services/528d18a7-e24f-432e-bf1d-842f534c44f0 HTTP/1.1" 200 OK
2026-02-01 21:49:13 -06:00: INFO: 127.0.0.1:53110 - "GET /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 21:49:38 -06:00: INFO: 127.0.0.1:19530 - "OPTIONS /api/admin/services/9c70d282-8b80-45d0-ba38-e93bd8f0479c HTTP/1.1" 200 OK
2026-02-01 21:49:38 -06:00: INFO: 127.0.0.1:19530 - "PUT /api/admin/services/9c70d282-8b80-45d0-ba38-e93bd8f0479c HTTP/1.1" 500 Internal Server Error
2026-02-01 22:03:42 -06:00: INFO: 127.0.0.1:45878 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 22:03:42 -06:00: INFO: 127.0.0.1:45886 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 22:03:42 -06:00: INFO: 127.0.0.1:45886 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 22:03:42 -06:00: INFO: 127.0.0.1:45886 - "OPTIONS /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 22:03:42 -06:00: INFO: 127.0.0.1:45878 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 22:03:42 -06:00: INFO: 127.0.0.1:45878 - "OPTIONS /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 22:03:42 -06:00: INFO: 127.0.0.1:45878 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 22:03:42 -06:00: INFO: 127.0.0.1:45890 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 22:03:45 -06:00: INFO: 127.0.0.1:45878 - "OPTIONS /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 22:03:45 -06:00: INFO: 127.0.0.1:45878 - "GET /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 22:04:30 -06:00: INFO: 127.0.0.1:45916 - "OPTIONS /api/admin/services/9c70d282-8b80-45d0-ba38-e93bd8f0479c HTTP/1.1" 200 OK
2026-02-01 22:04:30 -06:00: INFO: 127.0.0.1:45916 - "PUT /api/admin/services/9c70d282-8b80-45d0-ba38-e93bd8f0479c HTTP/1.1" 200 OK
2026-02-01 22:04:30 -06:00: INFO: 127.0.0.1:45916 - "GET /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 22:06:26 -06:00: INFO: 127.0.0.1:59192 - "OPTIONS /api/admin/services/d1633bbb-dc79-400e-95ce-2c91554d58e8 HTTP/1.1" 200 OK
2026-02-01 22:06:26 -06:00: INFO: 127.0.0.1:59192 - "PUT /api/admin/services/d1633bbb-dc79-400e-95ce-2c91554d58e8 HTTP/1.1" 200 OK
2026-02-01 22:06:26 -06:00: INFO: 127.0.0.1:59192 - "GET /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 22:10:24 -06:00: INFO: 127.0.0.1:55494 - "OPTIONS /api/admin/services/e9fdd79e-7728-4ca5-a961-b9aa2be10124 HTTP/1.1" 200 OK
2026-02-01 22:10:25 -06:00: INFO: 127.0.0.1:55508 - "PUT /api/admin/services/e9fdd79e-7728-4ca5-a961-b9aa2be10124 HTTP/1.1" 200 OK
2026-02-01 22:10:25 -06:00: INFO: 127.0.0.1:55508 - "GET /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 22:11:27 -06:00: INFO: 127.0.0.1:43970 - "OPTIONS /api/admin/services/fa6c750f-f1c1-406f-bf16-769106d2bfbf HTTP/1.1" 200 OK
2026-02-01 22:11:27 -06:00: INFO: 127.0.0.1:43972 - "PUT /api/admin/services/fa6c750f-f1c1-406f-bf16-769106d2bfbf HTTP/1.1" 200 OK
2026-02-01 22:11:27 -06:00: INFO: 127.0.0.1:43972 - "GET /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 22:12:46 -06:00: INFO: 127.0.0.1:63840 - "PUT /api/admin/services/fa6c750f-f1c1-406f-bf16-769106d2bfbf HTTP/1.1" 200 OK
2026-02-01 22:12:46 -06:00: INFO: 127.0.0.1:63840 - "GET /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 22:12:52 -06:00: INFO: 127.0.0.1:63856 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 22:12:52 -06:00: INFO: 127.0.0.1:63856 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 22:12:52 -06:00: INFO: 127.0.0.1:63856 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 22:12:52 -06:00: INFO: 127.0.0.1:63856 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 22:12:54 -06:00: INFO: 127.0.0.1:63856 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 22:13:30 -06:00: INFO: 127.0.0.1:6322 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 22:13:30 -06:00: INFO: 127.0.0.1:6320 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 22:13:32 -06:00: INFO: 127.0.0.1:6320 - "GET /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 22:13:58 -06:00: INFO: 127.0.0.1:11176 - "GET /api/services/9c70d282-8b80-45d0-ba38-e93bd8f0479c HTTP/1.1" 200 OK
2026-02-01 22:14:05 -06:00: INFO: 127.0.0.1:11186 - "OPTIONS /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 22:14:05 -06:00: INFO: 127.0.0.1:11186 - "OPTIONS /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 22:14:06 -06:00: INFO: 127.0.0.1:55478 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 22:14:06 -06:00: INFO: 127.0.0.1:55464 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 22:14:09 -06:00: INFO: 127.0.0.1:55464 - "OPTIONS /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 22:14:09 -06:00: INFO: 127.0.0.1:55464 - "GET /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 22:14:21 -06:00: INFO: 127.0.0.1:55490 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 22:14:25 -06:00: INFO: 127.0.0.1:55490 - "GET /api/services/e9fdd79e-7728-4ca5-a961-b9aa2be10124 HTTP/1.1" 200 OK
2026-02-01 22:19:04 -06:00: INFO: 127.0.0.1:46468 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 22:19:05 -06:00: INFO: 127.0.0.1:46482 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 22:19:05 -06:00: INFO: 127.0.0.1:46468 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 22:19:05 -06:00: INFO: 127.0.0.1:46486 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 22:19:05 -06:00: INFO: 127.0.0.1:46482 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 22:19:09 -06:00: INFO: 127.0.0.1:46482 - "GET /api/services/e9fdd79e-7728-4ca5-a961-b9aa2be10124 HTTP/1.1" 200 OK
2026-02-01 22:19:20 -06:00: INFO: 127.0.0.1:46496 - "GET /api/services/d1633bbb-dc79-400e-95ce-2c91554d58e8 HTTP/1.1" 200 OK
2026-02-01 22:19:23 -06:00: INFO: 127.0.0.1:46496 - "GET /api/services/fff97750-c6ee-4350-80c0-87cf93f16c13 HTTP/1.1" 200 OK
2026-02-01 22:20:03 -06:00: INFO: 127.0.0.1:11218 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 22:20:03 -06:00: INFO: 127.0.0.1:11228 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 22:20:07 -06:00: INFO: 127.0.0.1:11228 - "GET /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 22:21:20 -06:00: INFO: 127.0.0.1:19538 - "OPTIONS /api/admin/services/fff97750-c6ee-4350-80c0-87cf93f16c13 HTTP/1.1" 200 OK
2026-02-01 22:21:20 -06:00: INFO: 127.0.0.1:19538 - "PUT /api/admin/services/fff97750-c6ee-4350-80c0-87cf93f16c13 HTTP/1.1" 200 OK
2026-02-01 22:21:20 -06:00: INFO: 127.0.0.1:19538 - "GET /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 22:22:31 -06:00: INFO: 127.0.0.1:24738 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 22:23:17 -06:00: INFO: 127.0.0.1:27426 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 22:23:47 -06:00: INFO: 127.0.0.1:47310 - "POST /api/auth/login HTTP/1.1" 401 Unauthorized
2026-02-01 22:24:02 -06:00: INFO: 127.0.0.1:45828 - "POST /api/auth/login HTTP/1.1" 401 Unauthorized
2026-02-01 22:25:26 -06:00: INFO: 127.0.0.1:23242 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 22:25:41 -06:00: INFO: 127.0.0.1:60338 - "POST /api/auth/login HTTP/1.1" 200 OK
2026-02-01 22:25:41 -06:00: INFO: 127.0.0.1:30518 - "PUT /api/admin/services/fa6c750f-f1c1-406f-bf16-769106d2bfbf HTTP/1.1" 200 OK
2026-02-01 22:28:27 -06:00: INFO: 127.0.0.1:12810 - "OPTIONS /api/auth/me HTTP/1.1" 200 OK
2026-02-01 22:28:27 -06:00: INFO: 127.0.0.1:12824 - "OPTIONS /api/cart HTTP/1.1" 200 OK
2026-02-01 22:28:27 -06:00: INFO: 127.0.0.1:12824 - "GET /api/auth/me HTTP/1.1" 200 OK
2026-02-01 22:28:27 -06:00: INFO: 127.0.0.1:12824 - "GET /api/cart HTTP/1.1" 200 OK
2026-02-01 22:28:27 -06:00: INFO: 127.0.0.1:12824 - "OPTIONS /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 22:28:27 -06:00: INFO: 127.0.0.1:12828 - "OPTIONS /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 22:28:27 -06:00: INFO: 127.0.0.1:12828 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 22:28:27 -06:00: INFO: 127.0.0.1:12828 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 22:28:30 -06:00: INFO: 127.0.0.1:12828 - "OPTIONS /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 22:28:30 -06:00: INFO: 127.0.0.1:12828 - "GET /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 22:28:44 -06:00: INFO: 127.0.0.1:12838 - "GET /api/services HTTP/1.1" 200 OK
2026-02-01 22:28:54 -06:00: INFO: 127.0.0.1:12856 - "GET /api/admin/categories HTTP/1.1" 200 OK
2026-02-01 22:28:54 -06:00: INFO: 127.0.0.1:12852 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
2026-02-01 22:28:58 -06:00: INFO: 127.0.0.1:12852 - "GET /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 22:29:14 -06:00: INFO: 127.0.0.1:12870 - "OPTIONS /api/admin/services/fa6c750f-f1c1-406f-bf16-769106d2bfbf HTTP/1.1" 200 OK
2026-02-01 22:29:14 -06:00: INFO: 127.0.0.1:12870 - "PUT /api/admin/services/fa6c750f-f1c1-406f-bf16-769106d2bfbf HTTP/1.1" 200 OK
2026-02-01 22:29:14 -06:00: INFO: 127.0.0.1:12870 - "GET /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 22:29:28 -06:00: INFO: 127.0.0.1:39314 - "OPTIONS /api/admin/products?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 22:29:29 -06:00: INFO: 127.0.0.1:39314 - "GET /api/admin/products?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 22:29:29 -06:00: INFO: 127.0.0.1:39314 - "GET /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 22:29:30 -06:00: INFO: 127.0.0.1:39314 - "GET /api/admin/products?include_inactive=true HTTP/1.1" 200 OK
2026-02-01 22:29:35 -06:00: INFO: 127.0.0.1:39328 - "GET /api/products HTTP/1.1" 200 OK

View File

@@ -54,3 +54,133 @@
2026-01-13 18:37:05 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-01-13 18:37:05 -06:00: (node:516319) [DEP_WEBPACK_DEV_SERVER_ON_AFTER_SETUP_MIDDLEWARE] DeprecationWarning: 'onAfterSetupMiddleware' option is deprecated. Please use the 'setupMiddlewares' option.
2026-01-13 18:37:05 -06:00: (node:516319) [DEP_WEBPACK_DEV_SERVER_ON_BEFORE_SETUP_MIDDLEWARE] DeprecationWarning: 'onBeforeSetupMiddleware' option is deprecated. Please use the 'setupMiddlewares' option.
2026-02-01 16:28:57 -06:00: (node:305102) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:28:57 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:28:58 -06:00: (node:305232) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:28:58 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:28:59 -06:00: (node:305267) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:28:59 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:29:00 -06:00: (node:305324) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:29:00 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:29:00 -06:00: (node:305380) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:29:00 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:29:01 -06:00: (node:305445) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:29:01 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:29:02 -06:00: (node:305732) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:29:02 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:29:03 -06:00: (node:305767) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:29:03 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:29:04 -06:00: (node:305804) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:29:04 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:29:05 -06:00: (node:305840) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:29:05 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:29:06 -06:00: (node:305878) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:29:06 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:29:06 -06:00: (node:305921) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:29:06 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:29:07 -06:00: (node:306039) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:29:07 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:29:08 -06:00: (node:306179) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:29:08 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:29:09 -06:00: (node:306554) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:29:09 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:29:10 -06:00: (node:306598) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:29:10 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:29:11 -06:00: (node:306668) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:29:11 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:30:41 -06:00: (node:310173) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:30:41 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:30:42 -06:00: (node:310223) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:30:42 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:30:43 -06:00: (node:310327) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:30:43 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:30:44 -06:00: (node:310388) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:30:44 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:30:45 -06:00: (node:310423) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:30:45 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:30:46 -06:00: (node:310461) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:30:46 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:30:47 -06:00: (node:310507) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:30:47 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:30:48 -06:00: (node:310551) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:30:48 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:30:48 -06:00: (node:310647) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:30:48 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:30:49 -06:00: (node:310704) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:30:49 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:30:50 -06:00: (node:310739) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:30:50 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:30:51 -06:00: (node:310778) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:30:51 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:30:52 -06:00: (node:310839) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:30:52 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:30:53 -06:00: (node:310879) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:30:53 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:30:53 -06:00: (node:310972) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:30:53 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:30:54 -06:00: (node:311031) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:30:54 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:33:44 -06:00: (node:317523) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:33:44 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:33:45 -06:00: (node:317636) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:33:45 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:33:46 -06:00: (node:317670) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:33:46 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:33:47 -06:00: (node:317750) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:33:47 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:33:48 -06:00: (node:317784) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:33:48 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:33:48 -06:00: (node:317822) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:33:48 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:33:49 -06:00: (node:317857) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:33:49 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:33:50 -06:00: (node:317970) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:33:50 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:33:51 -06:00: (node:318021) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:33:51 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:33:52 -06:00: (node:318088) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:33:52 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:33:53 -06:00: (node:318141) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:33:53 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:33:54 -06:00: (node:318179) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:33:54 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:33:55 -06:00: (node:318215) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:33:55 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:33:55 -06:00: (node:318313) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:33:55 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:33:56 -06:00: (node:318347) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:33:56 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:33:57 -06:00: (node:318412) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:33:57 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:35:22 -06:00: (node:322333) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:35:22 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:35:23 -06:00: (node:322412) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:35:23 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:35:24 -06:00: (node:322461) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:35:24 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:35:25 -06:00: (node:322506) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:35:25 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:35:26 -06:00: (node:322544) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:35:26 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:35:27 -06:00: (node:322649) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:35:27 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:35:28 -06:00: (node:322687) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:35:28 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:35:29 -06:00: (node:322741) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:35:29 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:35:29 -06:00: (node:322780) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:35:29 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:35:30 -06:00: (node:322814) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:35:30 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:35:31 -06:00: (node:322911) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:35:31 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:35:32 -06:00: (node:322953) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:35:32 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:35:33 -06:00: (node:322991) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:35:33 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:35:34 -06:00: (node:323063) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:35:34 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:35:35 -06:00: (node:323103) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:35:35 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-01 16:35:35 -06:00: (node:323138) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
2026-02-01 16:35:35 -06:00: (Use `node --trace-deprecation ...` to show where the warning was created)

View File

@@ -1102,3 +1102,328 @@
2026-01-13 18:37:07 -06:00: cached modules 5.02 MiB [cached] 1940 modules
2026-01-13 18:37:07 -06:00: runtime modules 29.2 KiB 15 modules
2026-01-13 18:37:07 -06:00: webpack 5.104.1 compiled successfully in 1353 ms
2026-02-01 16:28:55 -06:00:
2026-02-01 16:28:55 -06:00: > frontend@0.1.0 start
2026-02-01 16:28:55 -06:00: > craco start
2026-02-01 16:28:55 -06:00:
2026-02-01 16:28:57 -06:00: Something is already running on port 5300.
2026-02-01 16:28:57 -06:00:
2026-02-01 16:28:57 -06:00: > frontend@0.1.0 start
2026-02-01 16:28:57 -06:00: > craco start
2026-02-01 16:28:57 -06:00:
2026-02-01 16:28:58 -06:00: Something is already running on port 5300.
2026-02-01 16:28:58 -06:00:
2026-02-01 16:28:58 -06:00: > frontend@0.1.0 start
2026-02-01 16:28:58 -06:00: > craco start
2026-02-01 16:28:58 -06:00:
2026-02-01 16:28:59 -06:00: Something is already running on port 5300.
2026-02-01 16:28:59 -06:00:
2026-02-01 16:28:59 -06:00: > frontend@0.1.0 start
2026-02-01 16:28:59 -06:00: > craco start
2026-02-01 16:28:59 -06:00:
2026-02-01 16:29:00 -06:00: Something is already running on port 5300.
2026-02-01 16:29:00 -06:00:
2026-02-01 16:29:00 -06:00: > frontend@0.1.0 start
2026-02-01 16:29:00 -06:00: > craco start
2026-02-01 16:29:00 -06:00:
2026-02-01 16:29:00 -06:00: Something is already running on port 5300.
2026-02-01 16:29:01 -06:00:
2026-02-01 16:29:01 -06:00: > frontend@0.1.0 start
2026-02-01 16:29:01 -06:00: > craco start
2026-02-01 16:29:01 -06:00:
2026-02-01 16:29:01 -06:00: Something is already running on port 5300.
2026-02-01 16:29:01 -06:00:
2026-02-01 16:29:01 -06:00: > frontend@0.1.0 start
2026-02-01 16:29:01 -06:00: > craco start
2026-02-01 16:29:01 -06:00:
2026-02-01 16:29:02 -06:00: Something is already running on port 5300.
2026-02-01 16:29:02 -06:00:
2026-02-01 16:29:02 -06:00: > frontend@0.1.0 start
2026-02-01 16:29:02 -06:00: > craco start
2026-02-01 16:29:02 -06:00:
2026-02-01 16:29:03 -06:00: Something is already running on port 5300.
2026-02-01 16:29:03 -06:00:
2026-02-01 16:29:03 -06:00: > frontend@0.1.0 start
2026-02-01 16:29:03 -06:00: > craco start
2026-02-01 16:29:03 -06:00:
2026-02-01 16:29:04 -06:00: Something is already running on port 5300.
2026-02-01 16:29:04 -06:00:
2026-02-01 16:29:04 -06:00: > frontend@0.1.0 start
2026-02-01 16:29:04 -06:00: > craco start
2026-02-01 16:29:04 -06:00:
2026-02-01 16:29:05 -06:00: Something is already running on port 5300.
2026-02-01 16:29:05 -06:00:
2026-02-01 16:29:05 -06:00: > frontend@0.1.0 start
2026-02-01 16:29:05 -06:00: > craco start
2026-02-01 16:29:05 -06:00:
2026-02-01 16:29:06 -06:00: Something is already running on port 5300.
2026-02-01 16:29:06 -06:00:
2026-02-01 16:29:06 -06:00: > frontend@0.1.0 start
2026-02-01 16:29:06 -06:00: > craco start
2026-02-01 16:29:06 -06:00:
2026-02-01 16:29:06 -06:00: Something is already running on port 5300.
2026-02-01 16:29:07 -06:00:
2026-02-01 16:29:07 -06:00: > frontend@0.1.0 start
2026-02-01 16:29:07 -06:00: > craco start
2026-02-01 16:29:07 -06:00:
2026-02-01 16:29:07 -06:00: Something is already running on port 5300.
2026-02-01 16:29:07 -06:00:
2026-02-01 16:29:07 -06:00: > frontend@0.1.0 start
2026-02-01 16:29:07 -06:00: > craco start
2026-02-01 16:29:07 -06:00:
2026-02-01 16:29:08 -06:00: Something is already running on port 5300.
2026-02-01 16:29:08 -06:00:
2026-02-01 16:29:08 -06:00: > frontend@0.1.0 start
2026-02-01 16:29:08 -06:00: > craco start
2026-02-01 16:29:08 -06:00:
2026-02-01 16:29:09 -06:00: Something is already running on port 5300.
2026-02-01 16:29:09 -06:00:
2026-02-01 16:29:09 -06:00: > frontend@0.1.0 start
2026-02-01 16:29:09 -06:00: > craco start
2026-02-01 16:29:09 -06:00:
2026-02-01 16:29:10 -06:00: Something is already running on port 5300.
2026-02-01 16:29:10 -06:00:
2026-02-01 16:29:10 -06:00: > frontend@0.1.0 start
2026-02-01 16:29:10 -06:00: > craco start
2026-02-01 16:29:10 -06:00:
2026-02-01 16:29:11 -06:00: Something is already running on port 5300.
2026-02-01 16:30:41 -06:00:
2026-02-01 16:30:41 -06:00: > frontend@0.1.0 start
2026-02-01 16:30:41 -06:00: > craco start
2026-02-01 16:30:41 -06:00:
2026-02-01 16:30:41 -06:00: Something is already running on port 5300.
2026-02-01 16:30:42 -06:00:
2026-02-01 16:30:42 -06:00: > frontend@0.1.0 start
2026-02-01 16:30:42 -06:00: > craco start
2026-02-01 16:30:42 -06:00:
2026-02-01 16:30:42 -06:00: Something is already running on port 5300.
2026-02-01 16:30:42 -06:00:
2026-02-01 16:30:42 -06:00: > frontend@0.1.0 start
2026-02-01 16:30:42 -06:00: > craco start
2026-02-01 16:30:42 -06:00:
2026-02-01 16:30:43 -06:00: Something is already running on port 5300.
2026-02-01 16:30:43 -06:00:
2026-02-01 16:30:43 -06:00: > frontend@0.1.0 start
2026-02-01 16:30:43 -06:00: > craco start
2026-02-01 16:30:43 -06:00:
2026-02-01 16:30:44 -06:00: Something is already running on port 5300.
2026-02-01 16:30:44 -06:00:
2026-02-01 16:30:44 -06:00: > frontend@0.1.0 start
2026-02-01 16:30:44 -06:00: > craco start
2026-02-01 16:30:44 -06:00:
2026-02-01 16:30:45 -06:00: Something is already running on port 5300.
2026-02-01 16:30:45 -06:00:
2026-02-01 16:30:45 -06:00: > frontend@0.1.0 start
2026-02-01 16:30:45 -06:00: > craco start
2026-02-01 16:30:45 -06:00:
2026-02-01 16:30:46 -06:00: Something is already running on port 5300.
2026-02-01 16:30:46 -06:00:
2026-02-01 16:30:46 -06:00: > frontend@0.1.0 start
2026-02-01 16:30:46 -06:00: > craco start
2026-02-01 16:30:46 -06:00:
2026-02-01 16:30:47 -06:00: Something is already running on port 5300.
2026-02-01 16:30:47 -06:00:
2026-02-01 16:30:47 -06:00: > frontend@0.1.0 start
2026-02-01 16:30:47 -06:00: > craco start
2026-02-01 16:30:47 -06:00:
2026-02-01 16:30:48 -06:00: Something is already running on port 5300.
2026-02-01 16:30:48 -06:00:
2026-02-01 16:30:48 -06:00: > frontend@0.1.0 start
2026-02-01 16:30:48 -06:00: > craco start
2026-02-01 16:30:48 -06:00:
2026-02-01 16:30:48 -06:00: Something is already running on port 5300.
2026-02-01 16:30:48 -06:00:
2026-02-01 16:30:48 -06:00: > frontend@0.1.0 start
2026-02-01 16:30:48 -06:00: > craco start
2026-02-01 16:30:48 -06:00:
2026-02-01 16:30:49 -06:00: Something is already running on port 5300.
2026-02-01 16:30:49 -06:00:
2026-02-01 16:30:49 -06:00: > frontend@0.1.0 start
2026-02-01 16:30:49 -06:00: > craco start
2026-02-01 16:30:49 -06:00:
2026-02-01 16:30:50 -06:00: Something is already running on port 5300.
2026-02-01 16:30:50 -06:00:
2026-02-01 16:30:50 -06:00: > frontend@0.1.0 start
2026-02-01 16:30:50 -06:00: > craco start
2026-02-01 16:30:50 -06:00:
2026-02-01 16:30:51 -06:00: Something is already running on port 5300.
2026-02-01 16:30:51 -06:00:
2026-02-01 16:30:51 -06:00: > frontend@0.1.0 start
2026-02-01 16:30:51 -06:00: > craco start
2026-02-01 16:30:51 -06:00:
2026-02-01 16:30:52 -06:00: Something is already running on port 5300.
2026-02-01 16:30:52 -06:00:
2026-02-01 16:30:52 -06:00: > frontend@0.1.0 start
2026-02-01 16:30:52 -06:00: > craco start
2026-02-01 16:30:52 -06:00:
2026-02-01 16:30:53 -06:00: Something is already running on port 5300.
2026-02-01 16:30:53 -06:00:
2026-02-01 16:30:53 -06:00: > frontend@0.1.0 start
2026-02-01 16:30:53 -06:00: > craco start
2026-02-01 16:30:53 -06:00:
2026-02-01 16:30:53 -06:00: Something is already running on port 5300.
2026-02-01 16:30:54 -06:00:
2026-02-01 16:30:54 -06:00: > frontend@0.1.0 start
2026-02-01 16:30:54 -06:00: > craco start
2026-02-01 16:30:54 -06:00:
2026-02-01 16:30:54 -06:00: Something is already running on port 5300.
2026-02-01 16:33:43 -06:00:
2026-02-01 16:33:43 -06:00: > frontend@0.1.0 start
2026-02-01 16:33:43 -06:00: > craco start
2026-02-01 16:33:43 -06:00:
2026-02-01 16:33:44 -06:00: Something is already running on port 5300.
2026-02-01 16:33:44 -06:00:
2026-02-01 16:33:44 -06:00: > frontend@0.1.0 start
2026-02-01 16:33:44 -06:00: > craco start
2026-02-01 16:33:44 -06:00:
2026-02-01 16:33:45 -06:00: Something is already running on port 5300.
2026-02-01 16:33:45 -06:00:
2026-02-01 16:33:45 -06:00: > frontend@0.1.0 start
2026-02-01 16:33:45 -06:00: > craco start
2026-02-01 16:33:45 -06:00:
2026-02-01 16:33:46 -06:00: Something is already running on port 5300.
2026-02-01 16:33:46 -06:00:
2026-02-01 16:33:46 -06:00: > frontend@0.1.0 start
2026-02-01 16:33:46 -06:00: > craco start
2026-02-01 16:33:46 -06:00:
2026-02-01 16:33:47 -06:00: Something is already running on port 5300.
2026-02-01 16:33:47 -06:00:
2026-02-01 16:33:47 -06:00: > frontend@0.1.0 start
2026-02-01 16:33:47 -06:00: > craco start
2026-02-01 16:33:47 -06:00:
2026-02-01 16:33:48 -06:00: Something is already running on port 5300.
2026-02-01 16:33:48 -06:00:
2026-02-01 16:33:48 -06:00: > frontend@0.1.0 start
2026-02-01 16:33:48 -06:00: > craco start
2026-02-01 16:33:48 -06:00:
2026-02-01 16:33:48 -06:00: Something is already running on port 5300.
2026-02-01 16:33:49 -06:00:
2026-02-01 16:33:49 -06:00: > frontend@0.1.0 start
2026-02-01 16:33:49 -06:00: > craco start
2026-02-01 16:33:49 -06:00:
2026-02-01 16:33:49 -06:00: Something is already running on port 5300.
2026-02-01 16:33:49 -06:00:
2026-02-01 16:33:49 -06:00: > frontend@0.1.0 start
2026-02-01 16:33:49 -06:00: > craco start
2026-02-01 16:33:49 -06:00:
2026-02-01 16:33:50 -06:00: Something is already running on port 5300.
2026-02-01 16:33:50 -06:00:
2026-02-01 16:33:50 -06:00: > frontend@0.1.0 start
2026-02-01 16:33:50 -06:00: > craco start
2026-02-01 16:33:50 -06:00:
2026-02-01 16:33:51 -06:00: Something is already running on port 5300.
2026-02-01 16:33:51 -06:00:
2026-02-01 16:33:51 -06:00: > frontend@0.1.0 start
2026-02-01 16:33:51 -06:00: > craco start
2026-02-01 16:33:51 -06:00:
2026-02-01 16:33:52 -06:00: Something is already running on port 5300.
2026-02-01 16:33:52 -06:00:
2026-02-01 16:33:52 -06:00: > frontend@0.1.0 start
2026-02-01 16:33:52 -06:00: > craco start
2026-02-01 16:33:52 -06:00:
2026-02-01 16:33:53 -06:00: Something is already running on port 5300.
2026-02-01 16:33:53 -06:00:
2026-02-01 16:33:53 -06:00: > frontend@0.1.0 start
2026-02-01 16:33:53 -06:00: > craco start
2026-02-01 16:33:53 -06:00:
2026-02-01 16:33:54 -06:00: Something is already running on port 5300.
2026-02-01 16:33:54 -06:00:
2026-02-01 16:33:54 -06:00: > frontend@0.1.0 start
2026-02-01 16:33:54 -06:00: > craco start
2026-02-01 16:33:54 -06:00:
2026-02-01 16:33:55 -06:00: Something is already running on port 5300.
2026-02-01 16:33:55 -06:00:
2026-02-01 16:33:55 -06:00: > frontend@0.1.0 start
2026-02-01 16:33:55 -06:00: > craco start
2026-02-01 16:33:55 -06:00:
2026-02-01 16:33:55 -06:00: Something is already running on port 5300.
2026-02-01 16:33:55 -06:00:
2026-02-01 16:33:55 -06:00: > frontend@0.1.0 start
2026-02-01 16:33:55 -06:00: > craco start
2026-02-01 16:33:55 -06:00:
2026-02-01 16:33:56 -06:00: Something is already running on port 5300.
2026-02-01 16:33:56 -06:00:
2026-02-01 16:33:56 -06:00: > frontend@0.1.0 start
2026-02-01 16:33:56 -06:00: > craco start
2026-02-01 16:33:56 -06:00:
2026-02-01 16:33:57 -06:00: Something is already running on port 5300.
2026-02-01 16:35:22 -06:00:
2026-02-01 16:35:22 -06:00: > frontend@0.1.0 start
2026-02-01 16:35:22 -06:00: > craco start
2026-02-01 16:35:22 -06:00:
2026-02-01 16:35:22 -06:00: Something is already running on port 5300.
2026-02-01 16:35:23 -06:00:
2026-02-01 16:35:23 -06:00: > frontend@0.1.0 start
2026-02-01 16:35:23 -06:00: > craco start
2026-02-01 16:35:23 -06:00:
2026-02-01 16:35:23 -06:00: Something is already running on port 5300.
2026-02-01 16:35:23 -06:00:
2026-02-01 16:35:23 -06:00: > frontend@0.1.0 start
2026-02-01 16:35:23 -06:00: > craco start
2026-02-01 16:35:23 -06:00:
2026-02-01 16:35:24 -06:00: Something is already running on port 5300.
2026-02-01 16:35:24 -06:00:
2026-02-01 16:35:24 -06:00: > frontend@0.1.0 start
2026-02-01 16:35:24 -06:00: > craco start
2026-02-01 16:35:24 -06:00:
2026-02-01 16:35:25 -06:00: Something is already running on port 5300.
2026-02-01 16:35:25 -06:00:
2026-02-01 16:35:25 -06:00: > frontend@0.1.0 start
2026-02-01 16:35:25 -06:00: > craco start
2026-02-01 16:35:25 -06:00:
2026-02-01 16:35:26 -06:00: Something is already running on port 5300.
2026-02-01 16:35:26 -06:00:
2026-02-01 16:35:26 -06:00: > frontend@0.1.0 start
2026-02-01 16:35:26 -06:00: > craco start
2026-02-01 16:35:26 -06:00:
2026-02-01 16:35:27 -06:00: Something is already running on port 5300.
2026-02-01 16:35:27 -06:00:
2026-02-01 16:35:27 -06:00: > frontend@0.1.0 start
2026-02-01 16:35:27 -06:00: > craco start
2026-02-01 16:35:27 -06:00:
2026-02-01 16:35:28 -06:00: Something is already running on port 5300.
2026-02-01 16:35:28 -06:00:
2026-02-01 16:35:28 -06:00: > frontend@0.1.0 start
2026-02-01 16:35:28 -06:00: > craco start
2026-02-01 16:35:28 -06:00:
2026-02-01 16:35:29 -06:00: Something is already running on port 5300.
2026-02-01 16:35:29 -06:00:
2026-02-01 16:35:29 -06:00: > frontend@0.1.0 start
2026-02-01 16:35:29 -06:00: > craco start
2026-02-01 16:35:29 -06:00:
2026-02-01 16:35:29 -06:00: Something is already running on port 5300.
2026-02-01 16:35:30 -06:00:
2026-02-01 16:35:30 -06:00: > frontend@0.1.0 start
2026-02-01 16:35:30 -06:00: > craco start
2026-02-01 16:35:30 -06:00:
2026-02-01 16:35:30 -06:00: Something is already running on port 5300.
2026-02-01 16:35:30 -06:00:
2026-02-01 16:35:30 -06:00: > frontend@0.1.0 start
2026-02-01 16:35:30 -06:00: > craco start
2026-02-01 16:35:30 -06:00:
2026-02-01 16:35:31 -06:00: Something is already running on port 5300.
2026-02-01 16:35:31 -06:00:
2026-02-01 16:35:31 -06:00: > frontend@0.1.0 start
2026-02-01 16:35:31 -06:00: > craco start
2026-02-01 16:35:31 -06:00:
2026-02-01 16:35:32 -06:00: Something is already running on port 5300.
2026-02-01 16:35:32 -06:00:
2026-02-01 16:35:32 -06:00: > frontend@0.1.0 start
2026-02-01 16:35:32 -06:00: > craco start
2026-02-01 16:35:32 -06:00:
2026-02-01 16:35:33 -06:00: Something is already running on port 5300.
2026-02-01 16:35:33 -06:00:
2026-02-01 16:35:33 -06:00: > frontend@0.1.0 start
2026-02-01 16:35:33 -06:00: > craco start
2026-02-01 16:35:33 -06:00:
2026-02-01 16:35:34 -06:00: Something is already running on port 5300.
2026-02-01 16:35:34 -06:00:
2026-02-01 16:35:34 -06:00: > frontend@0.1.0 start
2026-02-01 16:35:34 -06:00: > craco start
2026-02-01 16:35:34 -06:00:
2026-02-01 16:35:35 -06:00: Something is already running on port 5300.
2026-02-01 16:35:35 -06:00:
2026-02-01 16:35:35 -06:00: > frontend@0.1.0 start
2026-02-01 16:35:35 -06:00: > craco start
2026-02-01 16:35:35 -06:00:
2026-02-01 16:35:35 -06:00: Something is already running on port 5300.

View File

@@ -8,6 +8,10 @@ server {
# Disable ModSecurity for this site
modsecurity off;
# Buffer sizes
large_client_header_buffers 4 32k;
client_header_buffer_size 8k;
# Logs
access_log /var/log/nginx/prompttech-access.log;
@@ -20,17 +24,18 @@ server {
gzip_comp_level 6;
gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;
# Proxy to React dev server
# Serve built React static files
root /media/pts/Website/PromptTech_Solution_Site/frontend/build;
index index.html index.htm;
location / {
proxy_pass http://127.0.0.1:5300;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
try_files $uri $uri/ /index.html;
}
# Static file caching
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# API proxy to FastAPI backend
@@ -53,4 +58,18 @@ server {
proxy_send_timeout 300s;
proxy_read_timeout 300s;
}
# Proxy uploads to backend (media files) - ^~ prevents regex match
location ^~ /uploads/ {
proxy_pass http://127.0.0.1:8181/uploads/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Cache uploaded media files
expires 1y;
add_header Cache-Control "public, immutable";
}
}

View File

@@ -0,0 +1,258 @@
SSUUMMMMAARRYY OOFF LLEESSSS CCOOMMMMAANNDDSS
Commands marked with * may be preceded by a number, _N.
Notes in parentheses indicate the behavior if _N is given.
A key preceded by a caret indicates the Ctrl key; thus ^K is ctrl-K.
h H Display this help.
q :q Q :Q ZZ Exit.
---------------------------------------------------------------------------
MMOOVVIINNGG
e ^E j ^N CR * Forward one line (or _N lines).
y ^Y k ^K ^P * Backward one line (or _N lines).
f ^F ^V SPACE * Forward one window (or _N lines).
b ^B ESC-v * Backward one window (or _N lines).
z * Forward one window (and set window to _N).
w * Backward one window (and set window to _N).
ESC-SPACE * Forward one window, but don't stop at end-of-file.
d ^D * Forward one half-window (and set half-window to _N).
u ^U * Backward one half-window (and set half-window to _N).
ESC-) RightArrow * Right one half screen width (or _N positions).
ESC-( LeftArrow * Left one half screen width (or _N positions).
ESC-} ^RightArrow Right to last column displayed.
ESC-{ ^LeftArrow Left to first column.
F Forward forever; like "tail -f".
ESC-F Like F but stop when search pattern is found.
r ^R ^L Repaint screen.
R Repaint screen, discarding buffered input.
---------------------------------------------------
Default "window" is the screen height.
Default "half-window" is half of the screen height.
---------------------------------------------------------------------------
SSEEAARRCCHHIINNGG
/_p_a_t_t_e_r_n * Search forward for (_N-th) matching line.
?_p_a_t_t_e_r_n * Search backward for (_N-th) matching line.
n * Repeat previous search (for _N-th occurrence).
N * Repeat previous search in reverse direction.
ESC-n * Repeat previous search, spanning files.
ESC-N * Repeat previous search, reverse dir. & spanning files.
ESC-u Undo (toggle) search highlighting.
ESC-U Clear search highlighting.
&_p_a_t_t_e_r_n * Display only matching lines.
---------------------------------------------------
A search pattern may begin with one or more of:
^N or ! Search for NON-matching lines.
^E or * Search multiple files (pass thru END OF FILE).
^F or @ Start search at FIRST file (for /) or last file (for ?).
^K Highlight matches, but don't move (KEEP position).
^R Don't use REGULAR EXPRESSIONS.
^W WRAP search if no match found.
---------------------------------------------------------------------------
JJUUMMPPIINNGG
g < ESC-< * Go to first line in file (or line _N).
G > ESC-> * Go to last line in file (or line _N).
p % * Go to beginning of file (or _N percent into file).
t * Go to the (_N-th) next tag.
T * Go to the (_N-th) previous tag.
{ ( [ * Find close bracket } ) ].
} ) ] * Find open bracket { ( [.
ESC-^F _<_c_1_> _<_c_2_> * Find close bracket _<_c_2_>.
ESC-^B _<_c_1_> _<_c_2_> * Find open bracket _<_c_1_>.
---------------------------------------------------
Each "find close bracket" command goes forward to the close bracket
matching the (_N-th) open bracket in the top line.
Each "find open bracket" command goes backward to the open bracket
matching the (_N-th) close bracket in the bottom line.
m_<_l_e_t_t_e_r_> Mark the current top line with <letter>.
M_<_l_e_t_t_e_r_> Mark the current bottom line with <letter>.
'_<_l_e_t_t_e_r_> Go to a previously marked position.
'' Go to the previous position.
^X^X Same as '.
ESC-M_<_l_e_t_t_e_r_> Clear a mark.
---------------------------------------------------
A mark is any upper-case or lower-case letter.
Certain marks are predefined:
^ means beginning of the file
$ means end of the file
---------------------------------------------------------------------------
CCHHAANNGGIINNGG FFIILLEESS
:e [_f_i_l_e] Examine a new file.
^X^V Same as :e.
:n * Examine the (_N-th) next file from the command line.
:p * Examine the (_N-th) previous file from the command line.
:x * Examine the first (or _N-th) file from the command line.
:d Delete the current file from the command line list.
= ^G :f Print current file name.
---------------------------------------------------------------------------
MMIISSCCEELLLLAANNEEOOUUSS CCOOMMMMAANNDDSS
-_<_f_l_a_g_> Toggle a command line option [see OPTIONS below].
--_<_n_a_m_e_> Toggle a command line option, by name.
__<_f_l_a_g_> Display the setting of a command line option.
___<_n_a_m_e_> Display the setting of an option, by name.
+_c_m_d Execute the less cmd each time a new file is examined.
!_c_o_m_m_a_n_d Execute the shell command with $SHELL.
|XX_c_o_m_m_a_n_d Pipe file between current pos & mark XX to shell command.
s _f_i_l_e Save input to a file.
v Edit the current file with $VISUAL or $EDITOR.
V Print version number of "less".
---------------------------------------------------------------------------
OOPPTTIIOONNSS
Most options may be changed either on the command line,
or from within less by using the - or -- command.
Options may be given in one of two forms: either a single
character preceded by a -, or a name preceded by --.
-? ........ --help
Display help (from command line).
-a ........ --search-skip-screen
Search skips current screen.
-A ........ --SEARCH-SKIP-SCREEN
Search starts just after target line.
-b [_N] .... --buffers=[_N]
Number of buffers.
-B ........ --auto-buffers
Don't automatically allocate buffers for pipes.
-c ........ --clear-screen
Repaint by clearing rather than scrolling.
-d ........ --dumb
Dumb terminal.
-D xx_c_o_l_o_r . --color=xx_c_o_l_o_r
Set screen colors.
-e -E .... --quit-at-eof --QUIT-AT-EOF
Quit at end of file.
-f ........ --force
Force open non-regular files.
-F ........ --quit-if-one-screen
Quit if entire file fits on first screen.
-g ........ --hilite-search
Highlight only last match for searches.
-G ........ --HILITE-SEARCH
Don't highlight any matches for searches.
-h [_N] .... --max-back-scroll=[_N]
Backward scroll limit.
-i ........ --ignore-case
Ignore case in searches that do not contain uppercase.
-I ........ --IGNORE-CASE
Ignore case in all searches.
-j [_N] .... --jump-target=[_N]
Screen position of target lines.
-J ........ --status-column
Display a status column at left edge of screen.
-k [_f_i_l_e] . --lesskey-file=[_f_i_l_e]
Use a lesskey file.
-K ........ --quit-on-intr
Exit less in response to ctrl-C.
-L ........ --no-lessopen
Ignore the LESSOPEN environment variable.
-m -M .... --long-prompt --LONG-PROMPT
Set prompt style.
-n -N .... --line-numbers --LINE-NUMBERS
Don't use line numbers.
-o [_f_i_l_e] . --log-file=[_f_i_l_e]
Copy to log file (standard input only).
-O [_f_i_l_e] . --LOG-FILE=[_f_i_l_e]
Copy to log file (unconditionally overwrite).
-p [_p_a_t_t_e_r_n] --pattern=[_p_a_t_t_e_r_n]
Start at pattern (from command line).
-P [_p_r_o_m_p_t] --prompt=[_p_r_o_m_p_t]
Define new prompt.
-q -Q .... --quiet --QUIET --silent --SILENT
Quiet the terminal bell.
-r -R .... --raw-control-chars --RAW-CONTROL-CHARS
Output "raw" control characters.
-s ........ --squeeze-blank-lines
Squeeze multiple blank lines.
-S ........ --chop-long-lines
Chop (truncate) long lines rather than wrapping.
-t [_t_a_g] .. --tag=[_t_a_g]
Find a tag.
-T [_t_a_g_s_f_i_l_e] --tag-file=[_t_a_g_s_f_i_l_e]
Use an alternate tags file.
-u -U .... --underline-special --UNDERLINE-SPECIAL
Change handling of backspaces.
-V ........ --version
Display the version number of "less".
-w ........ --hilite-unread
Highlight first new line after forward-screen.
-W ........ --HILITE-UNREAD
Highlight first new line after any forward movement.
-x [_N[,...]] --tabs=[_N[,...]]
Set tab stops.
-X ........ --no-init
Don't use termcap init/deinit strings.
-y [_N] .... --max-forw-scroll=[_N]
Forward scroll limit.
-z [_N] .... --window=[_N]
Set size of window.
-" [_c[_c]] . --quotes=[_c[_c]]
Set shell quote characters.
-~ ........ --tilde
Don't display tildes after end of file.
-# [_N] .... --shift=[_N]
Set horizontal scroll amount (0 = one half screen width).
--file-size
Automatically determine the size of the input file.
--follow-name
The F command changes files if the input file is renamed.
--incsearch
Search file as each pattern character is typed in.
--line-num-width=N
Set the width of the -N line number field to N characters.
--mouse
Enable mouse input.
--no-keypad
Don't send termcap keypad init/deinit strings.
--no-histdups
Remove duplicates from command history.
--rscroll=C
Set the character used to mark truncated lines.
--save-marks
Retain marks across invocations of less.
--status-col-width=N
Set the width of the -J status column to N characters.
--use-backslash
Subsequent options use backslash as escape char.
--use-color
Enables colored text.
--wheel-lines=N
Each click of the mouse wheel moves N lines.
---------------------------------------------------------------------------
LLIINNEE EEDDIITTIINNGG
These keys can be used to edit text being entered
on the "command line" at the bottom of the screen.
RightArrow ..................... ESC-l ... Move cursor right one character.
LeftArrow ...................... ESC-h ... Move cursor left one character.
ctrl-RightArrow ESC-RightArrow ESC-w ... Move cursor right one word.
ctrl-LeftArrow ESC-LeftArrow ESC-b ... Move cursor left one word.
HOME ........................... ESC-0 ... Move cursor to start of line.
END ............................ ESC-$ ... Move cursor to end of line.
BACKSPACE ................................ Delete char to left of cursor.
DELETE ......................... ESC-x ... Delete char under cursor.
ctrl-BACKSPACE ESC-BACKSPACE ........... Delete word to left of cursor.
ctrl-DELETE .... ESC-DELETE .... ESC-X ... Delete word under cursor.
ctrl-U ......... ESC (MS-DOS only) ....... Delete entire line.
UpArrow ........................ ESC-k ... Retrieve previous command line.
DownArrow ...................... ESC-j ... Retrieve next command line.
TAB ...................................... Complete filename & cycle.
SHIFT-TAB ...................... ESC-TAB Complete filename & reverse cycle.
ctrl-L ................................... Complete filename, list all.

91
scripts/commit_local.sh Executable file
View File

@@ -0,0 +1,91 @@
#!/bin/bash
# Local Git Commit Script
# This script commits changes to the local repository
echo "===================================="
echo "Local Git Commit Script"
echo "===================================="
echo ""
# Show current status
echo "Current Git Status:"
git status
echo ""
# Ask user for commit type
echo "Select what to commit:"
echo "1) All changes (including logs)"
echo "2) Code changes only (exclude logs)"
echo "3) Custom selection"
read -p "Enter your choice (1-3): " choice
case $choice in
1)
echo ""
echo "Adding all changes..."
git add .
;;
2)
echo ""
echo "Adding code changes (excluding logs)..."
git add frontend/public/favicon*.png
git add frontend/public/android-chrome-*.png
git add frontend/public/apple-touch-icon.png
git add frontend/public/manifest.json
git add frontend/public/favicon.ico
git add frontend/public/index.html
git add frontend/src/components/cards/ProductCard.js
git add frontend/src/pages/About.js
git add frontend/src/pages/Products.js
git add backend/create_favicons.py
git add backend/update_hero_image.py
;;
3)
echo ""
echo "Enter the files/folders to add (space-separated):"
read -p "> " files
git add $files
;;
*)
echo "Invalid choice. Exiting."
exit 1
;;
esac
echo ""
echo "Staged changes:"
git status
echo ""
# Get commit message from user
read -p "Enter commit message: " commit_message
if [ -z "$commit_message" ]; then
echo "Error: Commit message cannot be empty."
exit 1
fi
# Commit the changes
echo ""
echo "Committing changes..."
git commit -m "$commit_message"
if [ $? -eq 0 ]; then
echo ""
echo "✓ Changes committed successfully!"
echo ""
echo "Current status:"
git log -1 --oneline
echo ""
git status
else
echo ""
echo "✗ Commit failed!"
exit 1
fi
echo ""
echo "===================================="
echo "Commit completed successfully!"
echo "===================================="

37
scripts/quick_commit.sh Executable file
View File

@@ -0,0 +1,37 @@
#!/bin/bash
# Quick Git Commit Script
# Usage: ./quick_commit.sh "Your commit message"
if [ -z "$1" ]; then
echo "Usage: ./quick_commit.sh \"Your commit message\""
echo ""
echo "Current changes:"
git status --short
exit 1
fi
COMMIT_MSG="$1"
echo "Adding all changes (excluding logs)..."
# Add specific files (excluding logs)
git add frontend/public/*.png
git add frontend/public/*.ico
git add frontend/public/manifest.json
git add frontend/public/index.html
git add frontend/src/
git add backend/*.py
echo ""
echo "Committing: $COMMIT_MSG"
git commit -m "$COMMIT_MSG"
if [ $? -eq 0 ]; then
echo ""
echo "✓ Committed successfully!"
git log -1 --oneline
else
echo ""
echo "✗ Commit failed or nothing to commit"
fi

136
ycopg2 Normal file
View File

@@ -0,0 +1,136 @@
SSUUMMMMAARRYY OOFF LLEESSSS CCOOMMMMAANNDDSS
Commands marked with * may be preceded by a number, _N.
Notes in parentheses indicate the behavior if _N is given.
A key preceded by a caret indicates the Ctrl key; thus ^K is ctrl-K.
h H Display this help.
q :q Q :Q ZZ Exit.
---------------------------------------------------------------------------
MMOOVVIINNGG
e ^E j ^N CR * Forward one line (or _N lines).
y ^Y k ^K ^P * Backward one line (or _N lines).
f ^F ^V SPACE * Forward one window (or _N lines).
b ^B ESC-v * Backward one window (or _N lines).
z * Forward one window (and set window to _N).
w * Backward one window (and set window to _N).
ESC-SPACE * Forward one window, but don't stop at end-of-file.
d ^D * Forward one half-window (and set half-window to _N).
u ^U * Backward one half-window (and set half-window to _N).
ESC-) RightArrow * Right one half screen width (or _N positions).
ESC-( LeftArrow * Left one half screen width (or _N positions).
ESC-} ^RightArrow Right to last column displayed.
ESC-{ ^LeftArrow Left to first column.
F Forward forever; like "tail -f".
ESC-F Like F but stop when search pattern is found.
r ^R ^L Repaint screen.
R Repaint screen, discarding buffered input.
---------------------------------------------------
Default "window" is the screen height.
Default "half-window" is half of the screen height.
---------------------------------------------------------------------------
SSEEAARRCCHHIINNGG
/_p_a_t_t_e_r_n * Search forward for (_N-th) matching line.
?_p_a_t_t_e_r_n * Search backward for (_N-th) matching line.
n * Repeat previous search (for _N-th occurrence).
N * Repeat previous search in reverse direction.
ESC-n * Repeat previous search, spanning files.
ESC-N * Repeat previous search, reverse dir. & spanning files.
ESC-u Undo (toggle) search highlighting.
ESC-U Clear search highlighting.
&_p_a_t_t_e_r_n * Display only matching lines.
---------------------------------------------------
A search pattern may begin with one or more of:
^N or ! Search for NON-matching lines.
^E or * Search multiple files (pass thru END OF FILE).
^F or @ Start search at FIRST file (for /) or last file (for ?).
^K Highlight matches, but don't move (KEEP position).
^R Don't use REGULAR EXPRESSIONS.
^W WRAP search if no match found.
---------------------------------------------------------------------------
JJUUMMPPIINNGG
g < ESC-< * Go to first line in file (or line _N).
G > ESC-> * Go to last line in file (or line _N).
p % * Go to beginning of file (or _N percent into file).
t * Go to the (_N-th) next tag.
T * Go to the (_N-th) previous tag.
{ ( [ * Find close bracket } ) ].
} ) ] * Find open bracket { ( [.
ESC-^F _<_c_1_> _<_c_2_> * Find close bracket _<_c_2_>.
ESC-^B _<_c_1_> _<_c_2_> * Find open bracket _<_c_1_>.
---------------------------------------------------
Each "find close bracket" command goes forward to the close bracket
matching the (_N-th) open bracket in the top line.
Each "find open bracket" command goes backward to the open bracket
matching the (_N-th) close bracket in the bottom line.
m_<_l_e_t_t_e_r_> Mark the current top line with <letter>.
M_<_l_e_t_t_e_r_> Mark the current bottom line with <letter>.
'_<_l_e_t_t_e_r_> Go to a previously marked position.
'' Go to the previous position.
^X^X Same as '.
ESC-M_<_l_e_t_t_e_r_> Clear a mark.
---------------------------------------------------
A mark is any upper-case or lower-case letter.
Certain marks are predefined:
^ means beginning of the file
$ means end of the file
---------------------------------------------------------------------------
CCHHAANNGGIINNGG FFIILLEESS
:e [_f_i_l_e] Examine a new file.
^X^V Same as :e.
:n * Examine the (_N-th) next file from the command line.
:p * Examine the (_N-th) previous file from the command line.
:x * Examine the first (or _N-th) file from the command line.
:d Delete the current file from the command line list.
= ^G :f Print current file name.
---------------------------------------------------------------------------
MMIISSCCEELLLLAANNEEOOUUSS CCOOMMMMAANNDDSS
-_<_f_l_a_g_> Toggle a command line option [see OPTIONS below].
--_<_n_a_m_e_> Toggle a command line option, by name.
__<_f_l_a_g_> Display the setting of a command line option.
___<_n_a_m_e_> Display the setting of an option, by name.
+_c_m_d Execute the less cmd each time a new file is examined.
!_c_o_m_m_a_n_d Execute the shell command with $SHELL.
|XX_c_o_m_m_a_n_d Pipe file between current pos & mark XX to shell command.
s _f_i_l_e Save input to a file.
v Edit the current file with $VISUAL or $EDITOR.
V Print version number of "less".
---------------------------------------------------------------------------
OOPPTTIIOONNSS
Most options may be changed either on the command line,
or from within less by using the - or -- command.
Options may be given in one of two forms: either a single
character preceded by a -, or a name preceded by --.
-? ........ --help
Display help (from command line).
-a ........ --search-skip-screen
Search skips current screen.
-A ........ --SEARCH-SKIP-SCREEN
Search starts just after target line.
-b [_N] .... --buffers=[_N]
Number of buffers.
-B ........ --auto-buffers
Don't automatically allocate buffers for pipes.
-c ........ --clear-screen
Repaint by clearing rather than scrolling.
-d ........ --dumb
Dumb terminal.
-D xx_c_o_l_o_r . --color=xx_c_o_l_o_r
Set screen colors.
-e -E .... --quit-at-eof --QUIT-AT-EOF