Update to v1.0 of website code from kottgard-production-v1.zip

This commit is contained in:
root
2026-06-23 09:24:49 +00:00
parent bd885cf0fa
commit f1f53fe4df
60 changed files with 658 additions and 160 deletions
+130
View File
@@ -0,0 +1,130 @@
#!/usr/bin/env python3
"""Prepare a sharp, full-frame beef boneless product photo for the shop."""
from __future__ import annotations
import argparse
import urllib.request
from pathlib import Path
from PIL import Image, ImageEnhance, ImageFilter, ImageOps, ImageStat
ROOT = Path(__file__).resolve().parents[1]
SITE = ROOT / "public" / "images" / "site"
OUT_PATH = SITE / "beef-boneless.jpg"
SOURCE_CACHE = SITE / "_beef_boneless_src.jpg"
# High-res premium raw boneless beef (Unsplash)
PRIMARY_URL = (
"https://images.unsplash.com/photo-1603048297172-c92544798d5a"
"?auto=format&fit=crop&w=1800&h=1800&crop=center&q=95"
)
# Original product reference (lower res fallback)
FALLBACK_URL = (
"https://www.hotcurrymarket.fi/wp-content/uploads/2025/01/"
"FRESH-BEEF-BONELESS-WITHOUT-FAT-1KG.jpg"
)
OUTPUT_SIZE = (1400, 1400)
def download(url: str, dest: Path) -> None:
req = urllib.request.Request(
url,
headers={"User-Agent": "Mozilla/5.0 (compatible; Kottgard-site-builder/1.0)"},
)
with urllib.request.urlopen(req, timeout=60) as resp:
dest.write_bytes(resp.read())
def trim_near_white(img: Image.Image, tolerance: int = 18) -> Image.Image:
"""Remove excess light borders from the source photo."""
rgb = img.convert("RGB")
pixels = rgb.load()
w, h = rgb.size
def row_is_border(y: int) -> bool:
samples = [pixels[x, y] for x in range(0, w, max(1, w // 24))]
return all(min(p) >= 255 - tolerance for p in samples)
def col_is_border(x: int) -> bool:
samples = [pixels[x, y] for y in range(0, h, max(1, h // 24))]
return all(min(p) >= 255 - tolerance for p in samples)
top = 0
while top < h and row_is_border(top):
top += 1
bottom = h - 1
while bottom > top and row_is_border(bottom):
bottom -= 1
left = 0
while left < w and col_is_border(left):
left += 1
right = w - 1
while right > left and col_is_border(right):
right -= 1
if right - left > 80 and bottom - top > 80:
return rgb.crop((left, top, right + 1, bottom + 1))
return rgb
def enhance_photo(img: Image.Image) -> Image.Image:
img = ImageOps.autocontrast(img, cutoff=0.5)
img = ImageEnhance.Brightness(img).enhance(1.03)
img = ImageEnhance.Contrast(img).enhance(1.08)
img = ImageEnhance.Color(img).enhance(1.1)
img = ImageEnhance.Sharpness(img).enhance(1.35)
return img.filter(ImageFilter.UnsharpMask(radius=1.0, percent=110, threshold=2))
def crop_center_cover(img: Image.Image, size: tuple[int, int]) -> Image.Image:
tw, th = size
w, h = img.size
scale = max(tw / w, th / h)
nw, nh = int(w * scale), int(h * scale)
resized = img.resize((nw, nh), Image.Resampling.LANCZOS)
left = (nw - tw) // 2
top = (nh - th) // 2
return resized.crop((left, top, left + tw, top + th))
def prepare_product_image(source: Path, output: Path = OUT_PATH) -> None:
beef = Image.open(source).convert("RGB")
beef = trim_near_white(beef)
beef = enhance_photo(beef)
# Full-frame crop — meat fills the product image (no tiny subject in white void)
beef = crop_center_cover(beef, OUTPUT_SIZE)
beef.save(output, "JPEG", quality=96, optimize=True, subsampling=0)
print(f"Saved product photo {OUTPUT_SIZE[0]}x{OUTPUT_SIZE[1]}{output}")
def main() -> None:
parser = argparse.ArgumentParser(description="Prepare beef boneless product photo")
parser.add_argument("--url", default=PRIMARY_URL)
parser.add_argument("--input", type=Path)
parser.add_argument("--output", type=Path, default=OUT_PATH)
parser.add_argument("--use-cache", action="store_true")
parser.add_argument("--fallback", action="store_true", help="Use hot curry source")
args = parser.parse_args()
if args.input:
src = args.input
elif args.use_cache and SOURCE_CACHE.exists():
src = SOURCE_CACHE
print(f"Using cached source → {src}")
else:
url = FALLBACK_URL if args.fallback else args.url
print(f"Downloading source image…\n {url}")
try:
download(url, SOURCE_CACHE)
except Exception as exc:
print(f"Primary download failed ({exc}), trying fallback…")
download(FALLBACK_URL, SOURCE_CACHE)
src = SOURCE_CACHE
prepare_product_image(src, args.output)
if __name__ == "__main__":
main()
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
# Production build must not reuse a dev .next folder.
set -euo pipefail
cd "$(dirname "$0")/.."
pkill -f "next dev" 2>/dev/null || true
rm -rf .next
echo "Building production bundle (clean cache)…"
exec npx next build
Executable
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
# Always start dev with a fresh .next cache — prevents missing vendor-chunks (zustand.js).
set -euo pipefail
cd "$(dirname "$0")/.."
pkill -f "next dev" 2>/dev/null || true
if lsof -ti:3000 >/dev/null 2>&1; then
lsof -ti:3000 | xargs kill -9 2>/dev/null || true
sleep 1
fi
rm -rf .next
echo "Starting Next.js dev server (clean cache)…"
exec npx next dev
+213
View File
@@ -0,0 +1,213 @@
#!/usr/bin/env python3
"""Normalize site images: square canvas, HALAL badge + Kött Gård logo. Colors preserved."""
from __future__ import annotations
import argparse
import shutil
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
ROOT = Path(__file__).resolve().parents[1]
SITE = ROOT / "public" / "images" / "site"
BACKUP = SITE / "_originals"
LOGO_PATH = SITE / "logo.jpeg"
BRAND_NAME = "Kött Gård"
SIZE = (1400, 1400)
BG = (255, 255, 255)
BURGUNDY = (139, 31, 31)
GOLD = (212, 175, 55)
SKIP = {"logo.jpeg", "README.txt"}
SKIP_PATTERNS = (" copy", "-1.jpg") # duplicate uploads from user
FILL_PRODUCT = 0.9
FILL_PAGE = 0.94
PAGE_IMAGES = {"hero.jpg", "about.jpg", "weekly-offers.jpg"}
CATEGORY_IMAGES = {
"category-chicken.jpg",
"category-beef.jpg",
"category-lamb.jpg",
"category-fish.jpg",
}
def load_font(size: int, bold: bool = False) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
paths = [
"/System/Library/Fonts/Supplemental/Arial Bold.ttf"
if bold
else "/System/Library/Fonts/Supplemental/Arial.ttf",
"/Library/Fonts/Arial.ttf",
]
for p in paths:
try:
return ImageFont.truetype(p, size)
except OSError:
continue
return ImageFont.load_default()
def preserve_colors(img: Image.Image) -> Image.Image:
"""Keep original photo pixels — no contrast, brightness, or color tweaks."""
return img.convert("RGB")
def prep_logo(diameter: int) -> Image.Image:
logo = Image.open(LOGO_PATH).convert("RGBA")
w, h = logo.size
side = min(w, h)
logo = logo.crop(((w - side) // 2, (h - side) // 2, (w + side) // 2, (h + side) // 2))
logo = logo.resize((diameter, diameter), Image.Resampling.LANCZOS)
mask = Image.new("L", (diameter, diameter), 0)
ImageDraw.Draw(mask).ellipse((0, 0, diameter - 1, diameter - 1), fill=255)
logo.putalpha(mask)
return logo
def draw_halal_badge(canvas: Image.Image, x: int, y: int, size: int = 130) -> None:
layer = Image.new("RGBA", canvas.size, (0, 0, 0, 0))
draw = ImageDraw.Draw(layer)
draw.ellipse((x + 4, y + 5, x + size + 4, y + size + 5), fill=(0, 0, 0, 100))
draw.ellipse((x - 2, y - 2, x + size + 2, y + size + 2), fill=(*GOLD, 255))
draw.ellipse((x + 6, y + 6, x + size - 6, y + size - 6), fill=(255, 255, 255, 255))
draw.ellipse((x + 10, y + 10, x + size - 10, y + size - 10), outline=(*BURGUNDY, 255), width=4)
f_ar = load_font(24, bold=True)
f_halal = load_font(28, bold=True)
f_sub = load_font(11, bold=True)
ar = draw.textbbox((0, 0), "حلال", font=f_ar)
draw.text((x + (size - ar[2] + ar[0]) // 2, y + 22), "حلال", fill=(*BURGUNDY, 255), font=f_ar)
hl = draw.textbbox((0, 0), "HALAL", font=f_halal)
draw.text((x + (size - hl[2] + hl[0]) // 2, y + 52), "HALAL", fill=(*BURGUNDY, 255), font=f_halal)
ce = draw.textbbox((0, 0), "CERTIFIED", font=f_sub)
draw.text((x + (size - ce[2] + ce[0]) // 2, y + 92), "CERTIFIED", fill=(*GOLD, 255), font=f_sub)
canvas.alpha_composite(layer)
def draw_logo_badge(canvas: Image.Image, logo: Image.Image, x: int, y: int) -> None:
d = logo.size[0]
layer = Image.new("RGBA", canvas.size, (0, 0, 0, 0))
draw = ImageDraw.Draw(layer)
draw.ellipse((x + 3, y + 4, x + d + 3, y + d + 4), fill=(0, 0, 0, 90))
draw.ellipse((x - 4, y - 4, x + d + 4, y + d + 4), fill=(255, 255, 255, 245), outline=(*GOLD, 255), width=3)
canvas.alpha_composite(layer)
canvas.alpha_composite(logo, (x, y))
def draw_brand_name(canvas: Image.Image, x: int, y: int) -> None:
"""Brand name pill beside the logo badge."""
layer = Image.new("RGBA", canvas.size, (0, 0, 0, 0))
draw = ImageDraw.Draw(layer)
font = load_font(22, bold=True)
bbox = draw.textbbox((0, 0), BRAND_NAME, font=font)
tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1]
pad_x, pad_y = 14, 8
box = (x - tw - pad_x * 2, y, x - 8, y + th + pad_y * 2)
draw.rounded_rectangle(box, radius=10, fill=(255, 255, 255, 235), outline=(*GOLD, 255), width=2)
draw.text((box[0] + pad_x, box[1] + pad_y - 2), BRAND_NAME, fill=(*BURGUNDY, 255), font=font)
canvas.alpha_composite(layer)
def already_branded(path: Path) -> bool:
"""Skip images already at catalog size (user uploads with logos baked in)."""
with Image.open(path) as img:
return img.size == SIZE
def should_skip(path: Path) -> bool:
if path.name in SKIP or path.name.startswith("_"):
return True
return any(p in path.name for p in SKIP_PATTERNS)
def compose(source: Path, dest: Path, logo: Image.Image) -> None:
photo = preserve_colors(Image.open(source))
fill = FILL_PAGE if dest.name in PAGE_IMAGES else FILL_PRODUCT
if dest.name in CATEGORY_IMAGES:
fill = 0.92
canvas = Image.new("RGBA", SIZE, (*BG, 255))
max_side = int(SIZE[0] * fill)
photo.thumbnail((max_side, max_side), Image.Resampling.LANCZOS)
shadow = Image.new("RGBA", photo.size, (0, 0, 0, 0))
sh_draw = ImageDraw.Draw(shadow)
sh_draw.rounded_rectangle(
(8, 12, photo.width - 8, photo.height - 4),
radius=18,
fill=(0, 0, 0, 35),
)
px = (SIZE[0] - photo.width) // 2
py = (SIZE[1] - photo.height) // 2 - 16
canvas.alpha_composite(shadow, (px, py + 6))
canvas.alpha_composite(photo.convert("RGBA"), (px, py))
margin = 28
draw_halal_badge(canvas, margin, margin)
logo_size = 108
logo_x = SIZE[0] - logo_size - margin
logo_y = SIZE[1] - logo_size - margin
draw_logo_badge(canvas, logo, logo_x, logo_y)
draw_brand_name(canvas, logo_x, logo_y + logo_size // 2 - 12)
canvas.convert("RGB").save(dest, "JPEG", quality=94, optimize=True, subsampling=0)
def ensure_catalog_files() -> None:
pairs = [
("beef-steak.jpg", "beef-steak-2.jpg"),
("beef-nihari-2.jpg", "beef-nihari.jpg"),
("category-lamb.jpg", "lamb-shoulder.jpg"),
]
for target, source in pairs:
t, s = SITE / target, SITE / source
if not t.exists() and s.exists():
shutil.copy2(s, t)
print(f"Created missing {target} from {source}")
def main() -> None:
parser = argparse.ArgumentParser(description="Brand site images (colors preserved)")
parser.add_argument("--force", action="store_true", help="Re-process even if already 1400×1400")
parser.add_argument("--only", nargs="*", help="Process only these filenames")
args = parser.parse_args()
if not LOGO_PATH.exists():
raise SystemExit(f"Missing logo: {LOGO_PATH}")
BACKUP.mkdir(exist_ok=True)
ensure_catalog_files()
logo = prep_logo(108)
files = sorted(SITE.glob("*.jpg"))
processed = 0
skipped = 0
for path in files:
if should_skip(path):
continue
if args.only and path.name not in args.only:
continue
if not args.force and already_branded(path):
print(f"{path.name} (already branded, skipped)")
skipped += 1
continue
backup = BACKUP / path.name
if not backup.exists():
shutil.copy2(path, backup)
source = backup if backup.exists() else path
compose(source, path, logo)
print(f"{path.name}")
processed += 1
print(f"\nDone — {processed} processed, {skipped} skipped. Originals in {BACKUP}")
if __name__ == "__main__":
main()