Author SHA1 Message Date
Linus OlofssonandClaude Opus 5.5 63baa35b92 Start page, portfolio, privacy and 404: Linus's list of 2026-11-06, and Swedish/English on every page
- Start page: the text block ("Fotograf Linus Olofsson ..." and "Posters och samarbeten ...") is gone; the buttons
  Posters, Samarbeten, Mer om mig, Portfolio and Kontakt sit at the bottom as long, straight buttons (no round pills, no
  text links). The second scroll box is Linus's own from May again (Redo att samarbeta? -> Kontakta mig) instead of
  "Vill du ha en bild på väggen?".
- Portfolio: "Vill du ha en bild på väggen? Se posters." and every "Köp som poster" button are gone. Photos are
  protected like on Posters: no link to the large file (nothing to open in a new tab), a transparent layer over every
  photo, no context menu, no drag, no long-press save.
- SV/EN: SV/EN switch in the menu; every visible text in both languages (menu, footer, cookie box, start page,
  portfolio with dates and months, privacy page, 404). The choice uses the portal's key (ibo_lang), so all pages on
  imagesbyolofsson.se follow the same choice; it is set before the page is drawn.
- Privacy: the statistics count openings only (there are no downloads any more).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 06:22:52 +01:00
6 changed files with 258 additions and 96 deletions
+12
View File
@@ -0,0 +1,12 @@
---
// src/components/T.astro – en text på svenska och engelska (Linus 2026-11-06: en svensk och en engelsk version,
// SV/EN-knappen i menyn växlar). Båda finns i sidan; CSS i MainLayout visar den som hör till <html lang>.
// <T sv="Om mig" en="About me" /> enkel text
// <T><Fragment slot="sv">…</Fragment><Fragment slot="en">…</Fragment></T> text med länkar eller taggar
export interface Props {
sv?: string;
en?: string;
}
const { sv, en } = Astro.props;
---
<span class="i18n-sv"><slot name="sv">{sv}</slot></span><span class="i18n-en" lang="en"><slot name="en">{en}</slot></span>
+65 -15
View File
@@ -4,6 +4,7 @@ import type { ImageMetadata } from 'astro';
import { getImage } from 'astro:assets';
import SEO from '../components/SEO.astro';
import SchemaOrg from '../components/SchemaOrg.astro';
import T from '../components/T.astro';
import type { FaqItem } from '../data/faq';
import { business } from '../data/business';
import defaultOgImage from '../assets/images/3d-scroll/img5.jpg';
@@ -43,12 +44,14 @@ const ogImageUrl = new URL(og.src, site).toString();
const ogWidth = Number(og.attributes.width) || undefined;
const ogHeight = Number(og.attributes.height) || undefined;
// Samma meny på alla sidor (Linus 2026-11-06): Portfolio, Posters, Om mig, Kontakt.
const nav = [
{ href: '/portfolio', label: 'Portfolio' },
{ href: '/posters', label: 'Posters' },
{ href: '/about', label: 'Om mig' },
{ href: '/contact', label: 'Kontakt' },
{ href: '/portfolio', sv: 'Portfolio', en: 'Portfolio' },
{ href: '/posters', sv: 'Posters', en: 'Posters' },
{ href: '/about', sv: 'Om mig', en: 'About me' },
{ href: '/contact', sv: 'Kontakt', en: 'Contact' },
];
const langBtn = 'rounded-full px-2.5 py-1 text-xs font-semibold transition-colors text-gray-400 hover:text-white aria-pressed:bg-white aria-pressed:text-neutral-900';
---
<!doctype html>
@@ -61,6 +64,24 @@ const nav = [
<link rel="icon" type="image/png" sizes="512x512" href="/favicon-512.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/favicon-180.png" />
<link rel="sitemap" type="application/xml" href="/sitemap-index.xml" />
<!-- Språket väljs innan sidan ritas (ingen blinkning): ?lang=, sedan besökarens val (samma nyckel som kundportalen),
sedan webbläsaren. Svenska/nordiska webbläsare får svenska. -->
<script is:inline>
(function () {
var lang = null;
try { var q = new URLSearchParams(location.search).get('lang'); if (q === 'sv' || q === 'en') lang = q; } catch (e) {}
if (!lang) { try { var s = localStorage.getItem('ibo_lang'); if (s === 'sv' || s === 'en') lang = s; } catch (e) {} }
if (!lang) {
var b = ((navigator.languages && navigator.languages[0]) || navigator.language || 'sv').toLowerCase();
lang = /^(sv|nb|no|da|nn)/.test(b) ? 'sv' : 'en';
}
document.documentElement.lang = lang;
})();
</script>
<style is:global>
html[lang="en"] .i18n-sv { display: none !important; }
html:not([lang="en"]) .i18n-en { display: none !important; }
</style>
<SEO
title={title}
description={description}
@@ -92,11 +113,17 @@ const nav = [
Images by Olofsson
</a>
<ul class="flex space-x-4 md:space-x-6 text-sm font-medium text-gray-300">
{nav.map((item) => (
<li><a href={item.href} class="hover:text-white transition">{item.label}</a></li>
))}
</ul>
<div class="flex items-center gap-4 md:gap-6">
<ul class="flex space-x-4 md:space-x-6 text-sm font-medium text-gray-300">
{nav.map((item) => (
<li><a href={item.href} class="hover:text-white transition"><T sv={item.sv} en={item.en} /></a></li>
))}
</ul>
<div class="inline-flex items-center rounded-full border border-neutral-700 p-0.5" role="group" aria-label="Språk / Language">
<button type="button" data-lang-set="sv" class={langBtn} aria-label="Svenska">SV</button>
<button type="button" data-lang-set="en" class={langBtn} aria-label="English">EN</button>
</div>
</div>
</div>
</nav>
@@ -106,26 +133,49 @@ const nav = [
<footer class="border-t border-neutral-800 py-8 text-center text-gray-400 text-sm bg-neutral-900 relative z-40">
<p class="mb-2">
{business.brand} – {business.person.jobTitle.toLowerCase()} {business.person.name}, {business.country.name}
{business.brand} – <T sv={`fotograf ${business.person.name}, Sverige`} en={`photographer ${business.person.name}, Sweden`} />
&middot; <a href={`mailto:${business.email}`} class="text-gray-300 hover:text-white transition-colors">{business.email}</a>
&middot; <a href={business.social.instagram.url} target="_blank" rel="noopener noreferrer" class="text-gray-300 hover:text-white transition-colors">Instagram</a>
</p>
<p>&copy; {new Date().getFullYear()} {business.brand}. Alla rättigheter förbehållna. &middot; <a href="/privacy" class="text-gray-300 hover:text-white transition-colors">Integritetspolicy</a> &middot; <button type="button" data-consent-open class="text-gray-300 hover:text-white transition-colors">Cookie-inställningar</button></p>
<p>&copy; {new Date().getFullYear()} {business.brand}. <T sv="Alla rättigheter förbehållna." en="All rights reserved." /> &middot; <a href="/privacy" class="text-gray-300 hover:text-white transition-colors"><T sv="Integritetspolicy" en="Privacy policy" /></a> &middot; <button type="button" data-consent-open class="text-gray-300 hover:text-white transition-colors"><T sv="Cookie-inställningar" en="Cookie settings" /></button></p>
</footer>
<!-- Cookie consent: Meta Pixel laddas först efter "Godkänn" -->
<div data-consent-banner hidden class="fixed bottom-4 inset-x-4 md:left-auto md:right-6 md:max-w-md z-[60] bg-neutral-900/95 backdrop-blur-md border border-white/10 rounded-2xl p-5 shadow-2xl text-sm text-gray-300">
<p class="mb-4">
Jag använder Meta Pixel för att se hur besökare från Instagram och Facebook använder sajten. Den sätter cookies och skickar data till Meta.
<a href="/privacy" class="text-white underline hover:text-gray-300">Läs mer</a>
<T sv="Jag använder Meta Pixel för att se hur besökare från Instagram och Facebook använder sajten. Den sätter cookies och skickar data till Meta."
en="I use the Meta Pixel to see how visitors from Instagram and Facebook use the site. It sets cookies and sends data to Meta." />
<a href="/privacy" class="text-white underline hover:text-gray-300"><T sv="Läs mer" en="Read more" /></a>
</p>
<div class="flex gap-3 justify-end">
<button type="button" data-consent="denied" class="px-4 py-2 rounded-full border border-white/20 text-gray-300 hover:text-white hover:border-white/40 transition-colors">Avböj</button>
<button type="button" data-consent="granted" class="px-4 py-2 rounded-full bg-white text-neutral-900 font-medium hover:bg-gray-200 transition-colors">Godkänn</button>
<button type="button" data-consent="denied" class="px-4 py-2 rounded-full border border-white/20 text-gray-300 hover:text-white hover:border-white/40 transition-colors"><T sv="Avböj" en="Decline" /></button>
<button type="button" data-consent="granted" class="px-4 py-2 rounded-full bg-white text-neutral-900 font-medium hover:bg-gray-200 transition-colors"><T sv="Godkänn" en="Accept" /></button>
</div>
</div>
<script is:inline>
// SV/EN-knappen: sparar valet (samma nyckel som kundportalen), byter <html lang> och meddelar sidans skript.
(function () {
function show(lang) {
document.documentElement.lang = lang;
document.querySelectorAll('[data-lang-set]').forEach(function (b) {
b.setAttribute('aria-pressed', b.getAttribute('data-lang-set') === lang ? 'true' : 'false');
});
document.querySelectorAll('[data-aria-sv]').forEach(function (el) {
el.setAttribute('aria-label', el.getAttribute(lang === 'en' ? 'data-aria-en' : 'data-aria-sv'));
});
document.dispatchEvent(new CustomEvent('ibo-lang', { detail: lang }));
}
show(document.documentElement.lang === 'en' ? 'en' : 'sv');
document.querySelectorAll('[data-lang-set]').forEach(function (b) {
b.addEventListener('click', function () {
var lang = b.getAttribute('data-lang-set');
try { localStorage.setItem('ibo_lang', lang); } catch (e) {}
show(lang);
});
});
})();
(function () {
var KEY = 'ibo-consent';
var banner = document.querySelector('[data-consent-banner]');
+9 -8
View File
@@ -2,12 +2,13 @@
// src/pages/404.astro -> dist/404.html (servern svarar med status 404)
import MainLayout from '../layouts/MainLayout.astro';
import WaveBackground from '../components/WaveBackground.astro';
import T from '../components/T.astro';
const links = [
{ href: '/', label: 'Startsidan' },
{ href: '/portfolio', label: 'Portfolio' },
{ href: '/posters', label: 'Posters' },
{ href: '/contact', label: 'Kontakt' },
{ href: '/', sv: 'Startsidan', en: 'Home' },
{ href: '/portfolio', sv: 'Portfolio', en: 'Portfolio' },
{ href: '/posters', sv: 'Posters', en: 'Posters' },
{ href: '/contact', sv: 'Kontakt', en: 'Contact' },
];
---
@@ -19,15 +20,15 @@ const links = [
<WaveBackground opacity="opacity-60" />
<div class="container mx-auto px-4 py-20 md:py-32 relative z-10 text-center">
<p class="text-sm text-gray-400 uppercase tracking-widest mb-4">Fel 404</p>
<h1 class="text-4xl md:text-6xl font-bold mb-6">Sidan hittades inte</h1>
<p class="text-sm text-gray-400 uppercase tracking-widest mb-4"><T sv="Fel 404" en="Error 404" /></p>
<h1 class="text-4xl md:text-6xl font-bold mb-6"><T sv="Sidan hittades inte" en="Page not found" /></h1>
<p class="text-gray-300 text-lg max-w-xl mx-auto mb-10">
Sidan du letade efter finns inte längre, eller så har adressen ändrats. Här kan du hitta tillbaka:
<T sv="Sidan du letade efter finns inte längre, eller så har adressen ändrats. Här kan du hitta tillbaka:" en="The page you were looking for no longer exists, or its address has changed. Find your way back here:" />
</p>
<ul class="flex flex-wrap justify-center gap-4">
{links.map((l) => (
<li>
<a href={l.href} class="inline-block px-6 py-3 rounded-full border border-white/20 text-white hover:bg-white hover:text-black transition-colors">{l.label}</a>
<a href={l.href} class="inline-block px-6 py-3 rounded-full border border-white/20 text-white hover:bg-white hover:text-black transition-colors"><T sv={l.sv} en={l.en} /></a>
</li>
))}
</ul>
+23 -39
View File
@@ -3,7 +3,16 @@
import MainLayout from '../layouts/MainLayout.astro';
import ThreeDScroll from '../components/ThreeDScroll.astro';
import WaveBackground from '../components/WaveBackground.astro';
import T from '../components/T.astro';
import { business, basedIn, genreList } from '../data/business';
const footerButtons = [
{ href: '/posters', sv: 'Posters', en: 'Posters' },
{ href: '/contact', sv: 'Samarbeten', en: 'Collaborations' },
{ href: '/about', sv: 'Mer om mig', en: 'More about me' },
{ href: '/portfolio', sv: 'Portfolio', en: 'Portfolio' },
{ href: '/contact', sv: 'Kontakt', en: 'Contact' },
];
---
<MainLayout
@@ -43,7 +52,7 @@ import { business, basedIn, genreList } from '../data/business';
</div>
<p class="text-white/80 mt-4 text-base md:text-lg tracking-widest uppercase">Fotograf {business.person.name} &middot; {basedIn()}</p>
<p class="text-white/80 mt-4 text-base md:text-lg tracking-widest uppercase"><T sv={`Fotograf ${business.person.name} · ${basedIn()}`} en={`Photographer ${business.person.name} · Örebro, Sweden`} /></p>
<p id="scroll-arrow" aria-hidden="true" class="text-white/70 mt-6 text-lg font-light tracking-widest uppercase transition-opacity duration-300">↓</p>
</div>
@@ -58,9 +67,9 @@ import { business, basedIn, genreList } from '../data/business';
<div class="flex flex-col md:flex-row gap-12 md:gap-24 items-center text-center">
<div data-trail-box class="flex flex-col items-center px-6 py-4 rounded-2xl">
<h2 class="text-3xl md:text-4xl text-white font-bold mb-6">Letar du inspiration?</h2>
<h2 class="text-3xl md:text-4xl text-white font-bold mb-6"><T sv="Letar du inspiration?" en="Looking for inspiration?" /></h2>
<a href="/portfolio" class="inline-block px-8 py-3 bg-white text-black font-bold rounded-full hover:bg-gray-200 transition transform hover:scale-105">
Se min portfolio
<T sv="Se min portfolio" en="See my portfolio" />
</a>
</div>
@@ -86,48 +95,23 @@ import { business, basedIn, genreList } from '../data/business';
</div>
<div data-trail-box class="flex flex-col items-center px-6 py-4 rounded-2xl">
<h2 class="text-3xl md:text-4xl text-white font-bold mb-6">Vill du ha en bild på väggen?</h2>
<a href="/posters" class="inline-block px-8 py-3 bg-white text-black font-bold rounded-full hover:bg-gray-200 transition transform hover:scale-105">
Se posters
<h2 class="text-3xl md:text-4xl text-white font-bold mb-6"><T sv="Redo att samarbeta?" en="Ready to collaborate?" /></h2>
<a href="/contact" class="inline-block px-8 py-3 bg-white text-black font-bold rounded-full hover:bg-gray-200 transition transform hover:scale-105">
<T sv="Kontakta mig" en="Contact me" />
</a>
</div>
</div>
</div>
<!-- Fakta om vem, vad och var: läsbar text för besökare, sökmotorer och AI-assistenter. -->
<section id="om-images-by-olofsson" class="relative z-30 bg-neutral-900 border-t border-white/10 py-16 md:py-24">
<div class="container mx-auto px-4 max-w-5xl grid gap-12 md:grid-cols-2">
<div class="space-y-5 text-gray-300 text-lg leading-relaxed">
<h2 class="text-3xl md:text-4xl font-bold text-white">Fotograf {business.person.name}</h2>
<p>
Jag heter {business.person.name} och fotograferar under namnet {business.brand}.
Jag är baserad i {basedIn()} och fotograferar {genreList()}.
</p>
<p>{business.recognition}</p>
<p>
Jag fotograferar med en {business.camera} och redigerar i {business.editing}, med fokus på naturliga toner och kontrast.
</p>
<p class="flex flex-wrap gap-x-6 gap-y-2 text-base">
<a href="/about" class="text-white underline underline-offset-4 hover:text-gray-300">Mer om mig</a>
<a href="/portfolio" class="text-white underline underline-offset-4 hover:text-gray-300">Portfolio</a>
<a href={`mailto:${business.email}`} class="text-white underline underline-offset-4 hover:text-gray-300">{business.email}</a>
</p>
</div>
<div class="space-y-5">
<h2 class="text-3xl md:text-4xl font-bold text-white">Posters och samarbeten</h2>
<p class="text-gray-300 text-lg leading-relaxed">
Fotografierna finns som posters: välj motiv, storlek och material. Vill du skapa något tillsammans,
eller synas genom reklam eller sponsring, hör av dig om ett samarbete.
</p>
<div class="flex flex-wrap gap-4 pt-2">
<a href="/posters" class="inline-block px-6 py-3 bg-white text-black font-bold rounded-full hover:bg-gray-200 transition">Se posters</a>
<a href="/contact" class="inline-block px-6 py-3 border border-white text-white font-bold rounded-full hover:bg-white/10 transition">Samarbeten</a>
</div>
</div>
</div>
</section>
<!-- Länkar längst ner (Linus 2026-11-06): bara knappar, avlånga och raka, ingen text och inga textlänkar. -->
<nav id="lankar" aria-label="Mer på sajten" data-aria-sv="Mer på sajten" data-aria-en="More on the site" class="relative z-30 bg-neutral-900 border-t border-white/10 py-12 md:py-16">
<ul class="container mx-auto px-4 flex flex-wrap justify-center gap-3 md:gap-4">
{footerButtons.map((b) => (
<li><a href={b.href} class="inline-flex min-w-[11rem] items-center justify-center rounded-md border border-white/20 bg-white/[0.03] px-8 py-3 text-xs md:text-sm font-semibold uppercase tracking-[0.2em] text-white transition-colors hover:border-white hover:bg-white hover:text-neutral-900"><T sv={b.sv} en={b.en} /></a></li>
))}
</ul>
</nav>
<script>
// SVG Längd
+68 -30
View File
@@ -6,6 +6,7 @@ import exifr from 'exifr';
import fs from 'node:fs';
import path from 'node:path';
import WaveBackground from '../components/WaveBackground.astro';
import T from '../components/T.astro';
import { business, genreList } from '../data/business';
import { portfolioMeta } from '../data/portfolio-meta';
@@ -74,6 +75,9 @@ const galleryItems = await Promise.all(Object.entries(imageFiles).map(async ([re
const fullDate = hasDate
? dateObj.toLocaleDateString('sv-SE', { day: 'numeric', month: 'long', year: 'numeric' })
: '';
const fullDateEn = hasDate
? dateObj.toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' })
: '';
const shutterSpeed = formatShutterSpeed(exifData?.ExposureTime);
const focalLength = exifData?.FocalLength ? `${Math.round(exifData.FocalLength)}mm` : null;
@@ -100,7 +104,7 @@ const galleryItems = await Promise.all(Object.entries(imageFiles).map(async ([re
return {
img: file.default, key, alt, title: meta.title ?? null,
year, month, monthIndex, fullDate, model: exifData?.Model,
year, month, monthIndex, fullDate, fullDateEn, model: exifData?.Model,
fStop, shutterSpeed, iso, focalLength,
large, srcset: jpg.srcSet.attribute,
statKey,
@@ -111,7 +115,8 @@ const uniqueYears = [...new Set(galleryItems.map((item: any) => item.year))].sor
const allMonths = galleryItems.map((item: any) => ({ name: item.month, index: item.monthIndex }));
const uniqueMonths = [...new Map(allMonths.map(item => [item.name, item])).values()]
.sort((a: any, b: any) => a.index - b.index)
.map((item: any) => item.name);
.map((item: any) => ({ sv: item.name, en: new Date(2000, item.index, 1).toLocaleString('en-GB', { month: 'long' }) }));
const genresEn = 'portraits and people, nature and animals, city and architecture, macro and details, northern lights, and celebrations and family occasions';
---
<MainLayout
@@ -130,32 +135,35 @@ const uniqueMonths = [...new Map(allMonths.map(item => [item.name, item])).value
<div class="text-center mb-12">
<h1 class="text-4xl md:text-6xl font-bold mb-4">Portfolio</h1>
<p class="text-gray-300 max-w-2xl mx-auto">Ett urval av mina bilder i olika stilar – {genreList()}.
Vill du ha en bild på väggen? Se <a href="/posters" class="text-white underline underline-offset-4 hover:text-gray-300">posters</a>.</p>
<p class="text-gray-300 max-w-2xl mx-auto"><T sv={`Ett urval av mina bilder i olika stilar – ${genreList()}.`} en={`A selection of my photos in different styles – ${genresEn}.`} /></p>
</div>
<div class="flex flex-col items-center gap-6 mb-12">
<div class="flex flex-wrap justify-center gap-2">
<button type="button" class="filter-btn active" data-filter="all" data-type="year">Alla år</button>
<button type="button" class="filter-btn active" data-filter="all" data-type="year"><T sv="Alla år" en="All years" /></button>
{uniqueYears.map(year => (
<button type="button" class="filter-btn" data-filter={year} data-type="year">{year}</button>
))}
</div>
<div class="flex flex-wrap justify-center gap-2">
<button type="button" class="filter-btn active" data-filter="all" data-type="month">Alla månader</button>
<button type="button" class="filter-btn active" data-filter="all" data-type="month"><T sv="Alla månader" en="All months" /></button>
{uniqueMonths.map(month => (
<button type="button" class="filter-btn capitalize" data-filter={month} data-type="month">{month}</button>
<button type="button" class="filter-btn capitalize" data-filter={month.sv} data-type="month"><T sv={month.sv} en={month.en} /></button>
))}
</div>
</div>
<h2 class="sr-only">Bilder</h2>
<h2 class="sr-only"><T sv="Bilder" en="Photos" /></h2>
<div class="columns-1 md:columns-2 lg:columns-3 gap-6 space-y-6" id="gallery-grid">
{galleryItems.map((item: any, index: number) => (
<a
href={item.large}
class="gallery-item block break-inside-avoid relative group overflow-hidden rounded-lg cursor-pointer bg-neutral-800 shadow-md"
<div
role="button"
tabindex="0"
aria-label={item.fullDate ? `Visa bilden från ${item.fullDate}` : 'Visa bilden'}
data-aria-sv={item.fullDate ? `Visa bilden från ${item.fullDate}` : 'Visa bilden'}
data-aria-en={item.fullDateEn ? `View the photo from ${item.fullDateEn}` : 'View the photo'}
class="gallery-item protected block break-inside-avoid relative group overflow-hidden rounded-lg cursor-pointer bg-neutral-800 shadow-md"
data-year={item.year}
data-month={item.month}
data-src={item.large}
@@ -170,6 +178,7 @@ const uniqueMonths = [...new Map(allMonths.map(item => [item.name, item])).value
data-shutter={item.shutterSpeed}
data-focal={item.focalLength}
data-date={item.fullDate}
data-date-en={item.fullDateEn}
>
<Image
src={item.img}
@@ -179,20 +188,23 @@ const uniqueMonths = [...new Map(allMonths.map(item => [item.name, item])).value
sizes="(min-width: 1024px) 33vw, (min-width: 768px) 50vw, 100vw"
loading={index < 3 ? 'eager' : 'lazy'}
decoding="async"
class="w-full h-auto object-cover transform transition duration-700 group-hover:scale-105"
draggable="false"
class="pointer-events-none select-none w-full h-auto object-cover transform transition duration-700 group-hover:scale-105"
/>
<!-- Genomskinligt lager över bilden (som på Posters): högerklick och långt tryck träffar inte bilden. -->
<div class="absolute inset-0" aria-hidden="true"></div>
<div class="absolute inset-0 bg-gradient-to-t from-black/90 via-black/40 to-transparent opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100 transition duration-300 flex flex-col justify-end p-6 pointer-events-none">
{(item.title || item.fullDate) && <p class="text-white font-bold text-lg">{item.title ?? item.fullDate}</p>}
<p class="text-xs text-gray-300 uppercase tracking-widest mt-1">Visa bild</p>
{item.fullDate && <p class="text-white font-bold text-lg"><T sv={item.fullDate} en={item.fullDateEn} /></p>}
<p class="text-xs text-gray-300 uppercase tracking-widest mt-1"><T sv="Visa bild" en="View photo" /></p>
</div>
</a>
</div>
))}
</div>
<div id="no-results" class="hidden text-center text-gray-400 mt-20">
<p class="text-xl">Inga bilder hittades för denna tidpunkt.</p>
<button type="button" id="reset-btn" class="mt-4 text-white underline hover:text-gray-300">Visa alla bilder</button>
<p class="text-xl"><T sv="Inga bilder hittades för denna tidpunkt." en="No photos found for this time." /></p>
<button type="button" id="reset-btn" class="mt-4 text-white underline hover:text-gray-300"><T sv="Visa alla bilder" en="Show all photos" /></button>
</div>
</div>
@@ -200,16 +212,17 @@ const uniqueMonths = [...new Map(allMonths.map(item => [item.name, item])).value
<div id="lightbox" class="fixed inset-0 z-50 hidden bg-black/95 backdrop-blur-md flex items-center justify-center opacity-0 transition-opacity duration-300 p-2 md:p-8">
<div class="bg-neutral-900 w-full max-w-6xl h-[85vh] md:h-[90vh] flex flex-col rounded-xl overflow-hidden shadow-2xl relative border border-white/10">
<button type="button" id="lb-close" aria-label="Stäng" class="absolute top-4 right-4 z-50 bg-black/50 hover:bg-black/80 text-white p-2 rounded-full transition-colors">
<button type="button" id="lb-close" aria-label="Stäng" data-aria-sv="Stäng" data-aria-en="Close" class="absolute top-4 right-4 z-50 bg-black/50 hover:bg-black/80 text-white p-2 rounded-full transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</button>
<div class="flex-1 relative bg-black flex items-center justify-center overflow-hidden group">
<img id="lb-img" src="" alt="" sizes="100vw" class="max-h-full max-w-full w-full h-full object-contain p-2 md:p-4" />
<button type="button" id="lb-prev" aria-label="Föregående bild" class="absolute left-2 md:left-4 top-1/2 -translate-y-1/2 bg-black/40 hover:bg-black/70 text-white p-3 rounded-full transition-all">
<div class="protected flex-1 relative bg-black flex items-center justify-center overflow-hidden group">
<img id="lb-img" src="" alt="" sizes="100vw" draggable="false" class="pointer-events-none select-none max-h-full max-w-full w-full h-full object-contain p-2 md:p-4" />
<div class="absolute inset-0" aria-hidden="true"></div>
<button type="button" id="lb-prev" aria-label="Föregående bild" data-aria-sv="Föregående bild" data-aria-en="Previous photo" class="absolute left-2 md:left-4 top-1/2 -translate-y-1/2 bg-black/40 hover:bg-black/70 text-white p-3 rounded-full transition-all">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg>
</button>
<button type="button" id="lb-next" aria-label="Nästa bild" class="absolute right-2 md:right-4 top-1/2 -translate-y-1/2 bg-black/40 hover:bg-black/70 text-white p-3 rounded-full transition-all">
<button type="button" id="lb-next" aria-label="Nästa bild" data-aria-sv="Nästa bild" data-aria-en="Next photo" class="absolute right-2 md:right-4 top-1/2 -translate-y-1/2 bg-black/40 hover:bg-black/70 text-white p-3 rounded-full transition-all">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>
</button>
</div>
@@ -220,13 +233,10 @@ const uniqueMonths = [...new Map(allMonths.map(item => [item.name, item])).value
<p id="lb-model" class="text-gray-400 text-sm font-medium tracking-wide mt-1"></p>
</div>
<div class="flex flex-wrap items-center gap-4 md:gap-6">
<a href="/posters" class="inline-flex items-center gap-2 px-6 py-2.5 bg-neutral-700/80 hover:bg-neutral-600/80 text-gray-200 hover:text-white font-semibold text-xs uppercase tracking-widest rounded-full border border-black/60 shadow-lg shadow-black/30 ring-1 ring-white/5 transition-all duration-200 transform hover:scale-105">
Köp som poster
</a>
<div class="flex flex-wrap gap-3 md:gap-6 text-xs md:text-sm font-mono text-gray-300">
<div class="flex flex-col md:flex-row md:items-center gap-1"><span class="text-gray-400 uppercase text-[10px] font-bold">Brännvidd</span><span id="lb-focal">--</span></div>
<div class="flex flex-col md:flex-row md:items-center gap-1"><span class="text-gray-400 uppercase text-[10px] font-bold">Bländare</span><span id="lb-fstop">--</span></div>
<div class="flex flex-col md:flex-row md:items-center gap-1"><span class="text-gray-400 uppercase text-[10px] font-bold">Slutartid</span><span id="lb-shutter">--</span></div>
<div class="flex flex-col md:flex-row md:items-center gap-1"><span class="text-gray-400 uppercase text-[10px] font-bold"><T sv="Brännvidd" en="Focal length" /></span><span id="lb-focal">--</span></div>
<div class="flex flex-col md:flex-row md:items-center gap-1"><span class="text-gray-400 uppercase text-[10px] font-bold"><T sv="Bländare" en="Aperture" /></span><span id="lb-fstop">--</span></div>
<div class="flex flex-col md:flex-row md:items-center gap-1"><span class="text-gray-400 uppercase text-[10px] font-bold"><T sv="Slutartid" en="Shutter speed" /></span><span id="lb-shutter">--</span></div>
<div class="flex flex-col md:flex-row md:items-center gap-1"><span class="text-gray-400 uppercase text-[10px] font-bold">ISO</span><span id="lb-iso">--</span></div>
</div>
</div>
@@ -334,7 +344,8 @@ const uniqueMonths = [...new Map(allMonths.map(item => [item.name, item])).value
lbImg.alt = item.dataset.alt || '';
stat('open', statsItem(item));
if(lbDate) lbDate.innerText = [item.dataset.title, item.dataset.date].filter(Boolean).join(' – ');
const en = document.documentElement.lang === 'en';
if(lbDate) lbDate.innerText = (en ? item.dataset.dateEn : item.dataset.date) || '';
if(lbModel) lbModel.innerText = item.dataset.model || '';
if(lbFocal) lbFocal.innerText = item.dataset.focal || '--';
if(lbFstop) lbFstop.innerText = item.dataset.fstop || '--';
@@ -352,8 +363,28 @@ const uniqueMonths = [...new Map(allMonths.map(item => [item.name, item])).value
updateLightboxContent();
}
// Skydd som på Posters: ingen högerklick-meny, ingen dragning, inget långt tryck på bilderna.
document.addEventListener('contextmenu', (e) => {
if ((e.target as HTMLElement).closest('.protected')) e.preventDefault();
});
document.addEventListener('dragstart', (e) => {
if ((e.target as HTMLElement).closest('.protected')) e.preventDefault();
});
// Byter besökaren språk med ljuslådan öppen följer datumet med.
document.addEventListener('ibo-lang', () => {
if (!lightbox?.classList.contains('hidden')) updateLightboxContent();
});
// Event Listeners
items.forEach((item) => {
item.addEventListener('keydown', (e) => {
const key = (e as KeyboardEvent).key;
if (key !== 'Enter' && key !== ' ') return;
e.preventDefault();
updateVisibleItems();
const index = visibleItems.indexOf(item);
if (index !== -1) openLightbox(index);
});
item.classList.add('visible-item');
item.addEventListener('click', (e) => {
// Länken pekar på den stora bilden (crawlbar/öppningsbar i ny flik),
@@ -385,7 +416,7 @@ const uniqueMonths = [...new Map(allMonths.map(item => [item.name, item])).value
// Filter Listeners
buttons.forEach(btn => {
btn.addEventListener('click', (e) => {
const target = e.target as HTMLElement;
const target = btn as HTMLElement;
const type = target.dataset.type;
const value = target.dataset.filter;
if (!type || !value) return;
@@ -418,6 +449,13 @@ const uniqueMonths = [...new Map(allMonths.map(item => [item.name, item])).value
<style>
@reference "../styles/global.css";
.protected, .protected img {
-webkit-touch-callout: none;
-webkit-user-select: none;
user-select: none;
-webkit-user-drag: none;
}
.filter-btn {
@apply px-4 py-1.5 rounded-full border border-white/10 bg-neutral-800/50 text-sm text-gray-400 transition hover:bg-white hover:text-black hover:border-white;
}
+81 -4
View File
@@ -2,6 +2,7 @@
// src/pages/privacy.astro
import MainLayout from '../layouts/MainLayout.astro';
import WaveBackground from '../components/WaveBackground.astro';
import T from '../components/T.astro';
import { business } from '../data/business';
const link = 'text-white underline hover:text-gray-300';
@@ -19,10 +20,10 @@ const link = 'text-white underline hover:text-gray-300';
<div class="container mx-auto px-4 py-12 md:py-20 relative z-10">
<div data-trail-box class="bg-neutral-900/60 backdrop-blur-md border border-white/10 rounded-3xl p-8 md:p-12 shadow-2xl mb-20 max-w-3xl mx-auto">
<h1 class="text-4xl md:text-5xl font-bold leading-tight mb-2">Integritetspolicy</h1>
<p class="text-gray-400 mb-10">Senast uppdaterad: 27 oktober 2026</p>
<h1 class="text-4xl md:text-5xl font-bold leading-tight mb-2"><T sv="Integritetspolicy" en="Privacy policy" /></h1>
<p class="text-gray-400 mb-10"><T sv="Senast uppdaterad: 6 november 2026" en="Last updated: 6 November 2026" /></p>
<div class="space-y-8 text-gray-300 text-lg leading-relaxed">
<div class="i18n-sv space-y-8 text-gray-300 text-lg leading-relaxed">
<section class="space-y-3">
<h2 class="text-2xl font-bold text-white">Vem som är ansvarig</h2>
@@ -63,7 +64,7 @@ const link = 'text-white underline hover:text-gray-300';
<section class="space-y-3">
<h2 class="text-2xl font-bold text-white">Anonym statistik för portfolion</h2>
<p>
För att se vilka bilder som uppskattas räknar portfolion hur många gånger varje bild öppnas eller laddas ner, per dag.
För att se vilka bilder som uppskattas räknar portfolion hur många gånger varje bild öppnas, per dag.
Bara antalet sparas, på min egen server i Sverige. Inga cookies, ingen IP-adress, ingen webbläsarinformation och inget
som kan identifiera dig. Det sker utan samtycke eftersom inga uppgifter om dig samlas in.
</p>
@@ -101,6 +102,82 @@ const link = 'text-white underline hover:text-gray-300';
</section>
</div>
<div class="i18n-en space-y-8 text-gray-300 text-lg leading-relaxed" lang="en">
<section class="space-y-3">
<h2 class="text-2xl font-bold text-white">Who is responsible</h2>
<p>
{business.brand} is run by {business.person.name}, Sweden. Questions about privacy:
<a href={`mailto:${business.email}`} class={link}>{business.email}</a>.
</p>
</section>
<section class="space-y-3">
<h2 class="text-2xl font-bold text-white">This website</h2>
<ul class="list-disc pl-6 space-y-2">
<li>There are no user accounts and no contact forms on this website. Poster orders and collaborations are handled in the separate customer portal.</li>
<li>If you e-mail me, your address and message are only used to answer you and are not shared with anyone.</li>
<li>
The website is delivered through Cloudflare, which processes technical data such as your IP address to deliver
and protect the website. See <a href="https://www.cloudflare.com/privacypolicy/" target="_blank" rel="noopener noreferrer" class={link}>Cloudflare's privacy policy</a>.
</li>
<li>If you appear in one of my photos and want it removed, e-mail me and I will remove it.</li>
</ul>
</section>
<section class="space-y-3">
<h2 class="text-2xl font-bold text-white">Meta Pixel (only with your consent)</h2>
<p>
If you click "Accept" in the cookie box, the website loads the Meta Pixel from Meta Platforms Ireland Ltd.
I use it to see how visitors coming from my Instagram and Facebook use the website.
</p>
<ul class="list-disc pl-6 space-y-2">
<li>It sets cookies (for example <code>_fbp</code>) and sends data to Meta, such as the pages you visit, your IP address and information about your browser.</li>
<li>Meta may link this to your Facebook or Instagram account and may transfer data outside the EU. See
<a href="https://www.facebook.com/privacy/policy/" target="_blank" rel="noopener noreferrer" class={link}>Meta's privacy policy</a>.</li>
<li>If you click "Decline" or do nothing, the pixel is never loaded.</li>
<li>You can change your choice at any time through "Cookie settings" at the bottom of every page. Your choice is stored in your own browser.</li>
</ul>
</section>
<section class="space-y-3">
<h2 class="text-2xl font-bold text-white">Anonymous statistics for the portfolio</h2>
<p>
To see which photos are appreciated, the portfolio counts how many times each photo is opened, per day.
Only the count is stored, on my own server in Sweden. No cookies, no IP address, no browser information and nothing
that can identify you. This happens without consent because no data about you is collected.
</p>
</section>
<section class="space-y-3">
<h2 class="text-2xl font-bold text-white">Photo backup app (Google)</h2>
<p>
"townhouse-backup" is a private tool that only I use to back up my own photos to my own Google account.
</p>
<ul class="list-disc pl-6 space-y-2">
<li>Google Drive: the tool only accesses files it has created itself (a backup of my photo archive).</li>
<li>Google Photos: the tool only uploads my photos and puts them in albums it has created itself; it cannot see anything else in the library.</li>
<li>No data about anyone else is collected. Nothing is sold, shared with third parties, used for advertising or used to train AI models.</li>
<li>
townhouse-backup's use of information received from Google APIs adheres to the
<a href="https://developers.google.com/terms/api-services-user-data-policy" target="_blank" rel="noopener noreferrer" class={link}>Google API Services User Data Policy</a>,
including the Limited Use requirements.
</li>
</ul>
</section>
<section class="space-y-3">
<h2 class="text-2xl font-bold text-white">Your rights</h2>
<p>
Under the GDPR you can ask what personal data I hold about you and have it corrected or deleted – just e-mail me.
You can also complain to the Swedish Authority for Privacy Protection
(<a href="https://www.imy.se/" target="_blank" rel="noopener noreferrer" class={link}>IMY</a>).
</p>
</section>
</div>
</div>
</div>