Add favicon icons and update product/about pages

This commit is contained in:
2026-02-18 23:02:58 -06:00
parent 82428c5589
commit ca04de4885
15 changed files with 256 additions and 7 deletions

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

@@ -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: 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 523 B

View File

@@ -2,7 +2,17 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<!-- 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"
}

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

@@ -221,8 +221,11 @@ const About = () => {
<div className="relative">
<img
src={
content.hero?.image_url ||
"/uploads/media/aa5bcc15-3b1e-4ed8-8708-1a3dceb9494d.jpg"
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"
@@ -603,8 +606,11 @@ const About = () => {
<div className="relative mb-4 overflow-hidden rounded-2xl aspect-square">
<img
src={
member.image_url ||
"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=400"
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"

View File

@@ -388,7 +388,7 @@ const Products = () => {
}`}
>
{filteredProducts.map((product) => (
<ProductCard key={product.id} product={product} />
<ProductCard key={product.id} product={product} viewMode={viewMode} />
))}
</div>
</>