Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24a59c6e93 | ||
|
|
8c9afb65c3 | ||
|
|
dc2bda8c07 | ||
|
|
6bb56e6035 | ||
|
|
1b24607e02 | ||
|
|
85bd49b0c3 | ||
|
|
7207e351d4 | ||
|
|
03a70efcb6 | ||
|
|
15560445ce |
@@ -10,8 +10,8 @@
|
|||||||
# import ibo_www_redirect
|
# import ibo_www_redirect
|
||||||
# }
|
# }
|
||||||
# imagesbyolofsson.se {
|
# imagesbyolofsson.se {
|
||||||
# # Befintliga specialregler (t.ex. handle /download/original* -> Immich-proxyn)
|
# # Specialregler (på Cleura: handle för /about, /contact, /posters -> kundportalen,
|
||||||
# # ska ligga FÖRE importen, så att de vinner över try_files/file_server.
|
# # se kundportalens deploy/cleura/caddy/Caddyfile) ska ligga FÖRE importen.
|
||||||
# import ibo_site /srv/imagesbyolofsson/dist
|
# import ibo_site /srv/imagesbyolofsson/dist
|
||||||
# }
|
# }
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# deploy/cleura/install-portfolio-sync.sh – installs the portfolio sync on the Cleura server (run as root from a
|
||||||
|
# checkout of this repo). Needs /etc/portal/portfolio-sync.env (root 600) first; see portfolio-sync.sh for the keys.
|
||||||
|
# Undo: systemctl disable --now portfolio-sync.timer && rm /etc/systemd/system/portfolio-sync.{service,timer}
|
||||||
|
set -euo pipefail
|
||||||
|
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||||
|
ENVF=/etc/portal/portfolio-sync.env
|
||||||
|
[ -f "$ENVF" ] || { echo "$ENVF is missing - copy IMMICH_URL, PORTFOLIO_TAG_ID, IMMICH_API_KEY and GIT_URL there first"; exit 2; }
|
||||||
|
[ "$(stat -c %a "$ENVF")" = "600" ] || chmod 600 "$ENVF"
|
||||||
|
install -m 755 -o root -g root "$HERE/portfolio-sync.sh" /opt/portal/site/portfolio-sync.sh
|
||||||
|
install -m 644 -o root -g root "$HERE/portfolio-sync.service" /etc/systemd/system/portfolio-sync.service
|
||||||
|
install -m 644 -o root -g root "$HERE/portfolio-sync.timer" /etc/systemd/system/portfolio-sync.timer
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now portfolio-sync.timer
|
||||||
|
systemctl list-timers portfolio-sync.timer --no-pager
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Immich portfolio tag -> imagesbyolofsson-se repo -> live site (deploy/cleura/portfolio-sync.sh)
|
||||||
|
After=network-online.target docker.service
|
||||||
|
Wants=network-online.target
|
||||||
|
Requires=docker.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/opt/portal/site/portfolio-sync.sh
|
||||||
|
TimeoutStartSec=45min
|
||||||
|
Nice=10
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# deploy/cleura/portfolio-sync.sh – Immich "portfolio" tag -> this repo -> imagesbyolofsson.se, on the Cleura server.
|
||||||
|
#
|
||||||
|
# Replaces /opt/immich-sync/run-sync.sh on Nurem (started by n8n over SSH) when imagesbyolofsson.se moved to Cleura
|
||||||
|
# (Linus 2026-11-06: everything on the Cleura server). Same steps: fetch main, run scripts/portfolio-sync.mjs,
|
||||||
|
# commit + push to Gitea main when photos were added or removed, and then rebuild the live site with
|
||||||
|
# /opt/portal/site/build-live.sh (EXIF check included). Nothing is built when nothing changed.
|
||||||
|
#
|
||||||
|
# Installed by deploy/cleura/install-portfolio-sync.sh as /opt/portal/site/portfolio-sync.sh, run by
|
||||||
|
# portfolio-sync.timer every 20 minutes. Secrets: /etc/portal/portfolio-sync.env (root 600) with
|
||||||
|
# IMMICH_URL, PORTFOLIO_TAG_ID, IMMICH_API_KEY (read-only Immich key) and GIT_URL (Gitea URL that may push main,
|
||||||
|
# with user:token@ in it, as on Nurem).
|
||||||
|
#
|
||||||
|
# The Gitea token never lands on disk outside that env file: the clone's remote URL is stored without it, git gets
|
||||||
|
# it from a credential helper at fetch/push time, and the Node container (npm install runs third-party code) gets
|
||||||
|
# an empty tmpfs over .git and only the Immich variables.
|
||||||
|
set -euo pipefail
|
||||||
|
umask 077
|
||||||
|
ENVF=${PORTFOLIO_SYNC_ENV:-/etc/portal/portfolio-sync.env}
|
||||||
|
DIR=${PORTFOLIO_SYNC_DIR:-/opt/portal/site/sync/repo}
|
||||||
|
NODE=node:22.22.0-alpine@sha256:e4bf2a82ad0a4037d28035ae71529873c069b13eb0455466ae0bc13363826e34
|
||||||
|
|
||||||
|
exec 9>/run/lock/portfolio-sync.lock
|
||||||
|
flock -n 9 || { echo "[sync] already running"; exit 0; }
|
||||||
|
|
||||||
|
set -a; . "$ENVF"; set +a
|
||||||
|
export IMMICH_URL="${IMMICH_URL:-https://foto.imagesbyolofsson.se/api}"
|
||||||
|
export PORTFOLIO_TAG_ID="${PORTFOLIO_TAG_ID:-67d16b6c-0dea-42ef-a289-2c9e57fa5c5a}"
|
||||||
|
[ -n "${IMMICH_API_KEY:-}" ] || { echo "FATAL: IMMICH_API_KEY missing in $ENVF"; exit 2; }
|
||||||
|
[ -n "${GIT_URL:-}" ] || { echo "FATAL: GIT_URL missing in $ENVF"; exit 2; }
|
||||||
|
|
||||||
|
# Split GIT_URL into the clean URL (stored in .git/config) and user/token (kept in this process only).
|
||||||
|
read -r CLEAN_URL GIT_USER GIT_TOKEN < <(python3 - "$GIT_URL" <<'PY'
|
||||||
|
import sys, urllib.parse as u
|
||||||
|
p = u.urlsplit(sys.argv[1])
|
||||||
|
host = p.hostname + (":%d" % p.port if p.port else "")
|
||||||
|
print(u.urlunsplit((p.scheme, host, p.path, "", "")), u.unquote(p.username or "-"), u.unquote(p.password or "-"))
|
||||||
|
PY
|
||||||
|
)
|
||||||
|
unset GIT_URL
|
||||||
|
export GIT_USER GIT_TOKEN
|
||||||
|
# shellcheck disable=SC2016 # $GIT_USER/$GIT_TOKEN are expanded by the helper's shell, not here.
|
||||||
|
G=(git -c credential.helper= -c 'credential.helper=!f() { echo "username=$GIT_USER"; echo "password=$GIT_TOKEN"; }; f')
|
||||||
|
|
||||||
|
if [ ! -d "$DIR/.git" ]; then
|
||||||
|
mkdir -p "$(dirname "$DIR")"
|
||||||
|
"${G[@]}" clone -q "$CLEAN_URL" "$DIR"
|
||||||
|
fi
|
||||||
|
chmod 700 "$(dirname "$DIR")" "$DIR"
|
||||||
|
git -C "$DIR" remote set-url origin "$CLEAN_URL"
|
||||||
|
echo "[sync] fetch + reset main"
|
||||||
|
"${G[@]}" -C "$DIR" fetch -q origin main
|
||||||
|
git -C "$DIR" checkout -q -B main origin/main
|
||||||
|
git -C "$DIR" reset -q --hard origin/main
|
||||||
|
|
||||||
|
echo "[sync] scripts/portfolio-sync.mjs"
|
||||||
|
# portfolio-sync.mjs exits 1 when a single photo fails but still writes the others; those are committed below, and the
|
||||||
|
# run ends with exit 1 so systemd shows that something needs a look (journalctl -u portfolio-sync).
|
||||||
|
SYNC_RC=0
|
||||||
|
docker run --rm --memory 3g --cpus 2 --network bridge \
|
||||||
|
-e IMMICH_URL -e IMMICH_API_KEY -e PORTFOLIO_TAG_ID \
|
||||||
|
-v "$DIR":/app --mount type=tmpfs,destination=/app/.git -w /app "$NODE" \
|
||||||
|
sh -c 'npm install --no-audit --no-fund --loglevel=error >/dev/null && node scripts/portfolio-sync.mjs' || SYNC_RC=$?
|
||||||
|
|
||||||
|
if git -C "$DIR" status --porcelain -- src/assets/images/portfolio | grep -q .; then
|
||||||
|
git -C "$DIR" add -A src/assets/images/portfolio
|
||||||
|
git -C "$DIR" -c user.name="portfolio-sync (Cleura)" -c user.email="Admin@imagesbyolofsson.se" \
|
||||||
|
commit -q -m "chore(portfolio): sync $(date -u +%Y-%m-%d)"
|
||||||
|
"${G[@]}" -C "$DIR" push -q origin main
|
||||||
|
echo "[sync] pushed $(git -C "$DIR" rev-parse --short HEAD), rebuilding the live site"
|
||||||
|
/opt/portal/site/build-live.sh main
|
||||||
|
else
|
||||||
|
echo "[sync] no changes"
|
||||||
|
fi
|
||||||
|
if [ "$SYNC_RC" != 0 ]; then
|
||||||
|
echo "[sync] WARNING: portfolio-sync.mjs exited $SYNC_RC - some photos failed (see the lines with '!' above)"
|
||||||
|
exit "$SYNC_RC"
|
||||||
|
fi
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Portfolio sync every 20 minutes (Immich has no event when a photo is tagged, so it polls)
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=*:0/20
|
||||||
|
RandomizedDelaySec=60
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
@@ -8,8 +8,6 @@
|
|||||||
|
|
||||||
User-agent: *
|
User-agent: *
|
||||||
Allow: /
|
Allow: /
|
||||||
# Lösenordsskyddad nedladdning av originalfiler – inget att indexera.
|
|
||||||
Disallow: /download/
|
|
||||||
|
|
||||||
# AI-crawlers och träningsbotar: uttryckligen tillåtna.
|
# AI-crawlers och träningsbotar: uttryckligen tillåtna.
|
||||||
User-agent: GPTBot
|
User-agent: GPTBot
|
||||||
@@ -36,6 +34,5 @@ User-agent: YouBot
|
|||||||
User-agent: DuckAssistBot
|
User-agent: DuckAssistBot
|
||||||
User-agent: MistralAI-User
|
User-agent: MistralAI-User
|
||||||
Allow: /
|
Allow: /
|
||||||
Disallow: /download/
|
|
||||||
|
|
||||||
Sitemap: https://imagesbyolofsson.se/sitemap-index.xml
|
Sitemap: https://imagesbyolofsson.se/sitemap-index.xml
|
||||||
|
|||||||
@@ -77,7 +77,9 @@ function destName(asset) {
|
|||||||
async function downloadAndResize(asset, destPath) {
|
async function downloadAndResize(asset, destPath) {
|
||||||
const res = await api("GET", `/assets/${asset.id}/original`);
|
const res = await api("GET", `/assets/${asset.id}/original`);
|
||||||
const buf = Buffer.from(await res.arrayBuffer());
|
const buf = Buffer.from(await res.arrayBuffer());
|
||||||
await sharp(buf)
|
// Pixel shift and the 15000 px Instagram squares can exceed sharp's default limit (268 MP). JPEGs are shrunk while
|
||||||
|
// they are decoded, so memory stays low; the sync container still has a memory cap.
|
||||||
|
await sharp(buf, { limitInputPixels: 1_000_000_000 })
|
||||||
.rotate() // respektera EXIF-orientering
|
.rotate() // respektera EXIF-orientering
|
||||||
.resize(MAX_DIM, MAX_DIM, { fit: "inside", withoutEnlargement: true })
|
.resize(MAX_DIM, MAX_DIM, { fit: "inside", withoutEnlargement: true })
|
||||||
.withMetadata() // BEVARA EXIF (kritiskt för portfolions datum/kamera)
|
.withMetadata() // BEVARA EXIF (kritiskt för portfolions datum/kamera)
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ const graph: Record<string, unknown>[] = [
|
|||||||
priceRange: business.priceRange,
|
priceRange: business.priceRange,
|
||||||
founder: { '@id': ids.person },
|
founder: { '@id': ids.person },
|
||||||
employee: { '@id': ids.person },
|
employee: { '@id': ids.person },
|
||||||
sameAs: [business.social.instagram.url, business.social.flickr, business.social.lightroom],
|
sameAs: [business.social.instagram.url, business.social.facebook, business.social.flickr, business.social.lightroom],
|
||||||
potentialAction: {
|
potentialAction: {
|
||||||
'@type': 'ReserveAction',
|
'@type': 'ReserveAction',
|
||||||
name: 'Skicka en bokningsförfrågan',
|
name: 'Skicka en bokningsförfrågan',
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ export type Business = {
|
|||||||
/** Telefonnummer i format +46... – okänt */
|
/** Telefonnummer i format +46... – okänt */
|
||||||
phone: string | null;
|
phone: string | null;
|
||||||
country: { code: string; name: string };
|
country: { code: string; name: string };
|
||||||
/** Ort Linus utgår från – okänt */
|
/** Ort Linus utgår från (hans publika Flickr- och LinkedIn-profil, 2026-10-27) */
|
||||||
city: string | null;
|
city: string | null;
|
||||||
/** Län/region – okänt */
|
/** Län/region (LinkedIn, 2026-10-27) */
|
||||||
region: string | null;
|
region: string | null;
|
||||||
/** Område han tar uppdrag i (fritext, t.ex. "Stockholm med omnejd") – okänt */
|
/** Område han tar uppdrag i (fritext, t.ex. "Stockholm med omnejd") – okänt */
|
||||||
serviceArea: string | null;
|
serviceArea: string | null;
|
||||||
@@ -55,6 +55,7 @@ export type Business = {
|
|||||||
};
|
};
|
||||||
social: {
|
social: {
|
||||||
instagram: { handle: string; url: string };
|
instagram: { handle: string; url: string };
|
||||||
|
facebook: string;
|
||||||
flickr: string;
|
flickr: string;
|
||||||
lightroom: string;
|
lightroom: string;
|
||||||
linkedin: string;
|
linkedin: string;
|
||||||
@@ -71,11 +72,11 @@ export const business: Business = {
|
|||||||
name: 'Linus Olofsson',
|
name: 'Linus Olofsson',
|
||||||
jobTitle: 'Fotograf',
|
jobTitle: 'Fotograf',
|
||||||
},
|
},
|
||||||
email: 'info@imagesbyolofsson.se',
|
email: 'Admin@imagesbyolofsson.se',
|
||||||
phone: null,
|
phone: null,
|
||||||
country: { code: 'SE', name: 'Sverige' },
|
country: { code: 'SE', name: 'Sverige' },
|
||||||
city: null,
|
city: 'Örebro',
|
||||||
region: null,
|
region: 'Örebro län',
|
||||||
serviceArea: null,
|
serviceArea: null,
|
||||||
priceFromSek: null,
|
priceFromSek: null,
|
||||||
priceRange: null,
|
priceRange: null,
|
||||||
@@ -94,6 +95,7 @@ export const business: Business = {
|
|||||||
},
|
},
|
||||||
social: {
|
social: {
|
||||||
instagram: { handle: '@imagesbyolofsson', url: 'https://www.instagram.com/imagesbyolofsson/' },
|
instagram: { handle: '@imagesbyolofsson', url: 'https://www.instagram.com/imagesbyolofsson/' },
|
||||||
|
facebook: 'https://www.facebook.com/imagesbyolofsson',
|
||||||
flickr: 'https://www.flickr.com/photos/imagesbyolofsson/',
|
flickr: 'https://www.flickr.com/photos/imagesbyolofsson/',
|
||||||
lightroom: 'https://lightroom.adobe.com/u/linusolofsson1',
|
lightroom: 'https://lightroom.adobe.com/u/linusolofsson1',
|
||||||
linkedin: 'https://www.linkedin.com/in/linus-olofsson-/',
|
linkedin: 'https://www.linkedin.com/in/linus-olofsson-/',
|
||||||
|
|||||||
+593
-3
@@ -1,7 +1,8 @@
|
|||||||
// src/data/portfolio-meta.ts
|
// src/data/portfolio-meta.ts
|
||||||
// DATAKROK för riktiga alt-texter (och ev. titel/kategori) per portfoliobild.
|
// DATAKROK för riktiga alt-texter (och ev. titel/kategori) per portfoliobild.
|
||||||
// Fylls i senare (t.ex. av Imogen-agenten). Tom post = sidan använder en
|
// Ifylld 2026-10-27: alt-texter skrivna med visionsmodellen gemma-4-31b-it-fp8 (Cleura)
|
||||||
// neutral svensk reservtext byggd på datum, aldrig kameramodell eller "undefined".
|
// på nedskalade kopior, granskade manuellt mot bilderna. Saknas en post använder sidan
|
||||||
|
// en neutral svensk reservtext byggd på datum, aldrig kameramodell eller "undefined".
|
||||||
//
|
//
|
||||||
// Nyckel = bildens filnamn utan ändelse, exakt som i src/assets/images/portfolio/,
|
// Nyckel = bildens filnamn utan ändelse, exakt som i src/assets/images/portfolio/,
|
||||||
// t.ex. "immich-02533d10-07ca-4a39-8b8a-6648fe2ffcef-20260905-DSC07734"
|
// t.ex. "immich-02533d10-07ca-4a39-8b8a-6648fe2ffcef-20260905-DSC07734"
|
||||||
@@ -23,4 +24,593 @@ export type PortfolioMeta = {
|
|||||||
category?: string;
|
category?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const portfolioMeta: Record<string, PortfolioMeta> = {};
|
export const portfolioMeta: Record<string, PortfolioMeta> = {
|
||||||
|
"20240804-20241104-IMG_3861": {
|
||||||
|
alt: "En person sedd bakifrån under lövverk mot ett dimmigt fält och en lada i skymningen",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"20240819-Screenshot_2024-08-19-12-12-33-62_92460851df6f172a4592fca41cc2d2e6": {
|
||||||
|
alt: "Närbild av ett gulsvart bi med orange pollen på benet mot en mörk bakgrund",
|
||||||
|
category: "Makro och detaljer",
|
||||||
|
},
|
||||||
|
"20240901-20240901-_J9E8565": {
|
||||||
|
alt: "En slingrande grusväg genom en mörk skog med höga björkar i dämpat ljus",
|
||||||
|
category: "Natur och djur",
|
||||||
|
},
|
||||||
|
"20240901-20240901-_J9E8586": {
|
||||||
|
alt: "Ett djuprött löv i centrum mot en mörk och suddig grön bakgrund i dämpat ljus",
|
||||||
|
category: "Makro och detaljer",
|
||||||
|
},
|
||||||
|
"20240901-_J9E8446": {
|
||||||
|
alt: "Vita fröställningar i ett fält av torrt gräs med varma färger och mörk bakgrund",
|
||||||
|
category: "Natur och djur",
|
||||||
|
},
|
||||||
|
"20240903-20240903-WJ9E8891": {
|
||||||
|
alt: "Kurviga järnvägsspår i sepiafärgat ljus omgivna av mörka träd och en byggnad i bakgrunden",
|
||||||
|
category: "Stad och arkitektur",
|
||||||
|
},
|
||||||
|
"20240914-20240914-WJ9E0318": {
|
||||||
|
alt: "Silhuett av en person med långt hår mot en gyllene solnedgång över öppna fält",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"20240921-WJ9E1119": {
|
||||||
|
alt: "En man med rödbrunt lockigt hår och skägg i mörka kläder i en dämpad stadsmiljö",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"20241021-IMG_9545-Enhanced-NR": {
|
||||||
|
alt: "Närbild på torra bruna ekblad omgivna av gula och orangea höstfärger med mörk vinjettering",
|
||||||
|
category: "Makro och detaljer",
|
||||||
|
},
|
||||||
|
"20241101-_MG_0023-Enhanced-NR": {
|
||||||
|
alt: "Ett brunt höstlöv med glittrande vattendroppar mot en svart bakgrund i dramatiskt ljus",
|
||||||
|
category: "Makro och detaljer",
|
||||||
|
},
|
||||||
|
"20241111-_MG_9832": {
|
||||||
|
alt: "En mörk stadsgata med byggnader under en lila skymningshimmel och en lysande måne",
|
||||||
|
category: "Stad och arkitektur",
|
||||||
|
},
|
||||||
|
"20241117-_J9E0597": {
|
||||||
|
alt: "En brun häst med grimma i profil mot en gyllene solnedgång och suddiga träd",
|
||||||
|
category: "Natur och djur",
|
||||||
|
},
|
||||||
|
"20241117-_J9E0620": {
|
||||||
|
alt: "Närbild av en vit häst med brunt läderträns i ett varmt och mörkt motljus",
|
||||||
|
category: "Natur och djur",
|
||||||
|
},
|
||||||
|
"20241117-_J9E0621": {
|
||||||
|
alt: "En vit häst med brun grimma i ett mjukt ljus vid en solnedgång",
|
||||||
|
category: "Natur och djur",
|
||||||
|
},
|
||||||
|
"20241124-_MG_1066-Enhanced-NR": {
|
||||||
|
alt: "En närbild av vita, glittrande iskristaller mot en mörk och suddig bakgrund",
|
||||||
|
category: "Makro och detaljer",
|
||||||
|
},
|
||||||
|
"20250217-_J9E5859-Enhanced-NR": {
|
||||||
|
alt: "Torkade fröställningar i mörkt, stämningsfullt ljus med dova bruna och lila färger",
|
||||||
|
category: "Makro och detaljer",
|
||||||
|
},
|
||||||
|
"20250217-_J9E5864-Enhanced-NR": {
|
||||||
|
alt: "En rödbrun torkad blomställning i mjukt ljus mot en mörk suddig bakgrund",
|
||||||
|
category: "Makro och detaljer",
|
||||||
|
},
|
||||||
|
"20250217-_J9E5897": {
|
||||||
|
alt: "Gyllene ax av torkat gräs i varmt ljus mot en mörkblå suddig bakgrund",
|
||||||
|
category: "Natur och djur",
|
||||||
|
},
|
||||||
|
"20250315-WJ9E1852": {
|
||||||
|
alt: "Ett barn i grön vinterjacka och mörk mössa håller i något i dämpat utomhusljus",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"20250315-WJ9E1919": {
|
||||||
|
alt: "En man i mörkblå mössa och dunjacka i dämpat ljus mot en suddig bakgrund",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"20250315-WJ9E1967-Enhanced-NR": {
|
||||||
|
alt: "En kvinna i vit mössa och ett barn i grön jacka blåser såpbubblor utomhus",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"20250330-_MG_1223-Enhanced-NR": {
|
||||||
|
alt: "En person håller en svart kamera mot en suddig bakgrund av gula höstlöv",
|
||||||
|
category: "Makro och detaljer",
|
||||||
|
},
|
||||||
|
"20250413-WJ9E0212": {
|
||||||
|
alt: "En man kör en vagn dragen av två hästar på en stadsgata i sepia",
|
||||||
|
category: "Stad och arkitektur",
|
||||||
|
},
|
||||||
|
"20250510-WJ9E1439-Enhanced-NR": {
|
||||||
|
alt: "Ett barn i randig mössa och grön jacka håller en röd spade i en sandlåda",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"20250510-WJ9E1492-Enhanced-NR": {
|
||||||
|
alt: "En kvinna med glasögon och brun handske håller en grön mugg i dämpat ljus",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"20250510-WJ9E1546-Enhanced-NR": {
|
||||||
|
alt: "Ett barn med blont hår och blå ögon i mörkblå fleecejacka med fingret vid munnen",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"20250510-WJ9E1598-Enhanced-NR": {
|
||||||
|
alt: "Två vuxna och ett barn går hand i hand över en parkering med bilar",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"20250510-WJ9E1673-Enhanced-NR": {
|
||||||
|
alt: "Ett barn i grön väst håller i ratten på ett rött fordon med en man bakom",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"20250510-WJ9E1806-Enhanced-NR": {
|
||||||
|
alt: "En man håller ett barn vid ett stängsel i en grön utomhusmiljö med röda byggnader",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"20250510-WJ9E1817-Enhanced-NR": {
|
||||||
|
alt: "En brun höna med röd kam i profil mot en suddig grön bakgrund",
|
||||||
|
category: "Natur och djur",
|
||||||
|
},
|
||||||
|
"20250513-WJ9E0143-Enhanced-NR": {
|
||||||
|
alt: "En blå klassisk bil på en stadsgata i varmt kvällsljus med mörka skuggor",
|
||||||
|
category: "Stad och arkitektur",
|
||||||
|
},
|
||||||
|
"20250513-WJ9E0211-Enhanced-NR": {
|
||||||
|
alt: "Gula och röda hus vid ett stilla vatten med speglingar i varmt kvällsljus",
|
||||||
|
category: "Stad och arkitektur",
|
||||||
|
},
|
||||||
|
"20250726-20250726-WJ9E0854": {
|
||||||
|
alt: "En ryttare i mörk rustning på en vit häst framför en ljus byggnad och publik",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"20250726-20250726-WJ9E0979": {
|
||||||
|
alt: "En man i blå och gul rustning med ett svärd rider en brun häst utomhus",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"20250802-WJ9E1325": {
|
||||||
|
alt: "En humla och en vit fjäril på lila blommor i ett mjukt gyllene ljus",
|
||||||
|
category: "Makro och detaljer",
|
||||||
|
},
|
||||||
|
"20250802-WJ9E1390": {
|
||||||
|
alt: "En vit fjäril med svarta fläckar på vingarna vilar på en lila blomstängel",
|
||||||
|
category: "Natur och djur",
|
||||||
|
},
|
||||||
|
"20250802-WJ9E1396": {
|
||||||
|
alt: "En vit fjäril med svarta prickar vilar på en lila blomma i starkt solljus",
|
||||||
|
category: "Natur och djur",
|
||||||
|
},
|
||||||
|
"20250806-IMG_5565": {
|
||||||
|
alt: "En orange och svart humla på en lila klotformad blomma mot grön bakgrund",
|
||||||
|
category: "Makro och detaljer",
|
||||||
|
},
|
||||||
|
"20251017-WJ9E0289": {
|
||||||
|
alt: "En tom träbänk i en höstskog omgiven av gula löv och varmt ljus",
|
||||||
|
category: "Natur och djur",
|
||||||
|
},
|
||||||
|
"20251227-WJ9E0356": {
|
||||||
|
alt: "Frostiga växter och en tunn gren i varmt motljus från en sol i skogen",
|
||||||
|
category: "Natur och djur",
|
||||||
|
},
|
||||||
|
"20251227-WJ9E0361": {
|
||||||
|
alt: "Frostig växt i motljus med en gyllene sol mot en mörk och suddig skog",
|
||||||
|
category: "Natur och djur",
|
||||||
|
},
|
||||||
|
"20251227-WJ9E0362": {
|
||||||
|
alt: "Varmt solljus strömmar genom träden bakom ett trästaket i en mörk skog",
|
||||||
|
category: "Natur och djur",
|
||||||
|
},
|
||||||
|
"20251227-WJ9E0388": {
|
||||||
|
alt: "Gyllene solljus som lyser upp frostiga torra löv och grässtrån på en mark",
|
||||||
|
category: "Makro och detaljer",
|
||||||
|
},
|
||||||
|
"20251227-WJ9E0434": {
|
||||||
|
alt: "En stadsgata i varmt gyllene ljus med suddiga byggnader och träd vid solnedgången",
|
||||||
|
category: "Stad och arkitektur",
|
||||||
|
},
|
||||||
|
"20251227-WJ9E0450": {
|
||||||
|
alt: "Gyllene solljus över en tom stadsgata med silhuetter av träd och en husfasad",
|
||||||
|
category: "Stad och arkitektur",
|
||||||
|
},
|
||||||
|
"20251230-IMG20251230141046": {
|
||||||
|
alt: "Snöig gränd med stenhus prydd med stora röda julkulor och små gröna granar i krukor",
|
||||||
|
category: "Stad och arkitektur",
|
||||||
|
},
|
||||||
|
"20251231-IMG20251231113121": {
|
||||||
|
alt: "Gyllene solljus över en snöig kullerstensgata omgiven av färgglada hus i en stad",
|
||||||
|
category: "Stad och arkitektur",
|
||||||
|
},
|
||||||
|
"20251231-IMG20251231113417": {
|
||||||
|
alt: "En smal kullerstensgata mellan en vit husfasad och en hög stenmur under en molnig himmel",
|
||||||
|
category: "Stad och arkitektur",
|
||||||
|
},
|
||||||
|
"20251231-WJ9E0465": {
|
||||||
|
alt: "En mörk lykta i förgrunden med en suddig ljus byggnad och spiror i bakgrunden",
|
||||||
|
category: "Stad och arkitektur",
|
||||||
|
},
|
||||||
|
"20251231-WJ9E0490": {
|
||||||
|
alt: "Snöklädda granris med små varma och kalla ljuspunkter i en mörk vintermiljö",
|
||||||
|
category: "Makro och detaljer",
|
||||||
|
},
|
||||||
|
"20251231-WJ9E0495": {
|
||||||
|
alt: "Snötäckta barr med små varma lampor mot en mörk bakgrund med suddiga orangea ljus",
|
||||||
|
category: "Natur och djur",
|
||||||
|
},
|
||||||
|
"20251231-WJ9E0512": {
|
||||||
|
alt: "En smal kullerstensgata i en stad med gamla hus, snöfläckar och svala, mörka färger",
|
||||||
|
category: "Stad och arkitektur",
|
||||||
|
},
|
||||||
|
"20251231-WJ9E0552": {
|
||||||
|
alt: "Smal stadsgata med ett högt torn, en julgran och silhuetter av människor i varmt ljus",
|
||||||
|
category: "Stad och arkitektur",
|
||||||
|
},
|
||||||
|
"20260102-IMG20260102132702": {
|
||||||
|
alt: "Vy genom en mörk stenvägg mot ett snöigt landskap och en gyllene himmel",
|
||||||
|
category: "Stad och arkitektur",
|
||||||
|
},
|
||||||
|
"20260102-IMG20260102141620": {
|
||||||
|
alt: "Grå stenmur med trägallerier och ett torn med rött tak under ett snöfall",
|
||||||
|
category: "Stad och arkitektur",
|
||||||
|
},
|
||||||
|
"20260102-IMG20260102142048": {
|
||||||
|
alt: "En trägång längs en grå stenmur under ett snöfall med kala grenar i förgrunden",
|
||||||
|
category: "Stad och arkitektur",
|
||||||
|
},
|
||||||
|
"immich-02533d10-07ca-4a39-8b8a-6648fe2ffcef-20260905-DSC07734": {
|
||||||
|
alt: "Två barn springer och skrattar framför ett hus med rött plåttak och högt gräs",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"immich-04342446-23b4-4600-a93e-352d32b6eac1-20260905-DSC07933": {
|
||||||
|
alt: "Två barn leker med en grön vattenpump och en hink på en solig gräsmatta",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"immich-0617ce88-3f9c-4b5b-bec4-1a30cfd65602-20260905-DSC07895": {
|
||||||
|
alt: "Ett blont barn i grön tröja vid en grön vattenpump i varmt gyllene ljus",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"immich-07328282-e321-4216-8710-ee87dc65496e-20260905-DSC07730": {
|
||||||
|
alt: "Ett leende barn i blå t-shirt står på en grön gräsmatta omgiven av gröna träd",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"immich-07578a7d-3d6e-4dbe-8fec-0accff45258c-20260920-DSC08719-2": {
|
||||||
|
alt: "En gulsvart humla samlar pollen från en mörkröd blomma mot en suddig grön bakgrund",
|
||||||
|
category: "Natur och djur",
|
||||||
|
},
|
||||||
|
"immich-0be0c122-4e81-4e92-b229-4736e66b9c70-20260920-DSC08717-2": {
|
||||||
|
alt: "En svartgul humla på en djupröd blomma omgiven av gröna stjälkar i varmt solljus",
|
||||||
|
category: "Makro och detaljer",
|
||||||
|
},
|
||||||
|
"immich-0d7ccea4-22c2-4208-b487-ec09eb88158c-20260704-DSC03171": {
|
||||||
|
alt: "En man och en kvinna med solglasögon poserar bredvid en vit limousine i en stad",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"immich-0f559874-9212-45e1-8860-a9ebfe6d95c0-20260704-DSC03081": {
|
||||||
|
alt: "En man i vit skjorta och mörka byxor vid en vit limousine under gröna träd",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"immich-10a42a42-e7af-4f02-a0ca-d07a960bd094-20260605-_J9E5983": {
|
||||||
|
alt: "Närbild på en svartvit hund med bruna ögon bland suddiga rosa blommor och grönska",
|
||||||
|
category: "Natur och djur",
|
||||||
|
},
|
||||||
|
"immich-146b119f-7a18-47ca-b255-38242adc290e-20260621-Kopia_av_IMG_7877": {
|
||||||
|
alt: "En röd ekorre på en grov trädgren med mjukt ljus och suddig bakgrund",
|
||||||
|
category: "Natur och djur",
|
||||||
|
},
|
||||||
|
"immich-17c30f0a-3dea-425d-9fbe-336cc1ad88e2-20260621-Kopia_av_IMG_8045": {
|
||||||
|
alt: "En rödbrun ekorre på en trädgren omgiven av ljusgröna blad i ett ljust skogsparti",
|
||||||
|
category: "Natur och djur",
|
||||||
|
},
|
||||||
|
"immich-18db801d-5db9-49d0-83c0-cda75832be93-20260920-DSC08726": {
|
||||||
|
alt: "En gulsvart humla i flykt mot en mörkröd blomma med orange centrum i grönska",
|
||||||
|
category: "Natur och djur",
|
||||||
|
},
|
||||||
|
"immich-1ab2b2fe-dea6-4aa4-81c9-73fb8d4918c3-20240816-WJ9E37214520240816": {
|
||||||
|
alt: "En man och ett barn i solljus med suddiga gröna träd och färgglada ljusreflexer",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"immich-1ccacf42-3f11-46c6-a6ac-4e38c6ff96b5-20250413-WJ9E0212": {
|
||||||
|
alt: "Två hästar drar en vit vagn med människor längs en solig gata i en stad",
|
||||||
|
category: "Stad och arkitektur",
|
||||||
|
},
|
||||||
|
"immich-1dc2a516-5026-4f97-861d-3e450acb0948-20260606-_J9E6089": {
|
||||||
|
alt: "Orange och svart fjäril som vilar på vita blommor i mörklila bladverk",
|
||||||
|
category: "Natur och djur",
|
||||||
|
},
|
||||||
|
"immich-1e4b55d1-10c0-4431-bcbf-0b6207208e1c-20260905-DSC07775": {
|
||||||
|
alt: "En man i vit jacka hjälper ett barn med blå hjälm och röd cykel utomhus",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"immich-23f04539-f8eb-47f2-8d39-3d3e548e0f9b-20260905-DSC07842": {
|
||||||
|
alt: "Ett leende barn med blont hår i en grön pikétröja utomhus i mjukt ljus",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"immich-27924d4a-0c2e-4421-b8cd-067b857549ba-20240912-20240912-WJ9E9866": {
|
||||||
|
alt: "En bäck med vitt skummande vatten, mossiga stenar och torra löv under en mörk trädstam",
|
||||||
|
category: "Natur och djur",
|
||||||
|
},
|
||||||
|
"immich-2be918bc-db83-4024-a066-bf300e2bebc5-20240912-20240912-WJ9E9861": {
|
||||||
|
alt: "Rinnande vatten över gröna mossiga stenar omgivna av gräs och torra bruna löv",
|
||||||
|
category: "Natur och djur",
|
||||||
|
},
|
||||||
|
"immich-2db21106-a3be-4f2b-82a7-eb69eb0038eb-20240912-20240912-WJ9E0194": {
|
||||||
|
alt: "Ett rödbrunt höstlöv som vilar på en mörk texturerad yta mot en mörkblå bakgrund",
|
||||||
|
category: "Makro och detaljer",
|
||||||
|
},
|
||||||
|
"immich-302dfa7d-fe44-45a4-9dd7-6e2d0ceaf39d-20260905-DSC07999": {
|
||||||
|
alt: "En kvinna med guldglasögon och blont hår i en denimjacka mot en suddig bakgrund",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"immich-30952208-1c57-48b0-8917-8176ee374efe-20260905-DSC07908": {
|
||||||
|
alt: "Ett barn i grön tröja vattnar lila blommor i en trädgård bredvid en ljus bod",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"immich-30a4d915-e794-40d0-8517-73c385a5d9c9-20260621-squareblur_1781332089807_7": {
|
||||||
|
alt: "En person i mörk mantel sedd bakifrån genom lövverk mot ett grönt fält",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"immich-3539cda6-7495-4a51-b2d5-61d528fece0f-20260621-Kopia_av_IMG_7804": {
|
||||||
|
alt: "En brun ekorre med fluffig svans och vitt bröst mot en ljus bakgrund",
|
||||||
|
category: "Natur och djur",
|
||||||
|
},
|
||||||
|
"immich-38146653-9b12-4333-a4f7-b72b86c58f87-20250416-WJ9E0243": {
|
||||||
|
alt: "En beige skoter parkerad på en kullerstensgata i en stad i varmt, gyllene ljus",
|
||||||
|
category: "Stad och arkitektur",
|
||||||
|
},
|
||||||
|
"immich-39b95c2c-eeb7-4334-8771-47e726ca00a3-20240816-WJ9E37204620240816": {
|
||||||
|
alt: "En man och ett barn i naturligt ljus med en suddig grön bakgrund av träd",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"immich-3b58d401-1b27-4c89-8094-c5025e24b160-20260905-DSC07838": {
|
||||||
|
alt: "En man i ljus skjorta kramar ett barn med blå hjälm bland färgglada blommor",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"immich-3d77f859-13be-462b-92be-767aa1f74ee6-20260920-DSC08731": {
|
||||||
|
alt: "Ett svartgult bi på en mörkröd blomma med suddig bakgrund i varma färger",
|
||||||
|
category: "Natur och djur",
|
||||||
|
},
|
||||||
|
"immich-3eccb3dc-707f-4f8d-837b-d0db61358e31-20260905-DSC07598": {
|
||||||
|
alt: "En man med solglasögon och ljus skjorta står bredvid en röd bil mot grön bakgrund",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"immich-415532f5-deec-43d5-8cb5-a47f4f0abfbc-20260606-_J9E6061": {
|
||||||
|
alt: "En gulsvart humla i flykt mot rosa blomklasar mot en ljusblå himmel",
|
||||||
|
category: "Natur och djur",
|
||||||
|
},
|
||||||
|
"immich-46430956-07b8-4306-86a2-daf98daab847-20241018-20241018-IMG_8965": {
|
||||||
|
alt: "Gammal stenbyggnad med runt torn och tända lyktor reflekteras i mörkt vatten under grå himmel",
|
||||||
|
category: "Stad och arkitektur",
|
||||||
|
},
|
||||||
|
"immich-490a149a-df25-4b81-96fd-3dc2fee7bc24-20241018-20241018-IMG_8926": {
|
||||||
|
alt: "En mörk stenbro med valv över vatten med blå toner och orangea reflektioner",
|
||||||
|
category: "Stad och arkitektur",
|
||||||
|
},
|
||||||
|
"immich-4abbfa4b-6eba-466d-88f4-57c2e867df82-20260920-DSC08912-2": {
|
||||||
|
alt: "En röd blomma i fokus mot en mörk bakgrund med varma gyllene ljuscirklar",
|
||||||
|
category: "Natur och djur",
|
||||||
|
},
|
||||||
|
"immich-4bcab51e-f51f-460d-8df7-7096e70a76bb-20260905-DSC07811": {
|
||||||
|
alt: "Ett leende barn med blå hjälm på en röd cykel i en suddig utomhusmiljö",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"immich-4f60de4c-4e1f-445c-afd9-f911e1acf995-20260605-_J9E5968": {
|
||||||
|
alt: "En svartvit prickig hund i profil omgiven av suddigt grönt gräs i naturligt ljus",
|
||||||
|
category: "Natur och djur",
|
||||||
|
},
|
||||||
|
"immich-4f8a9f96-16bb-4275-887b-7958a34addab-20250413-WJ9E0196": {
|
||||||
|
alt: "En gammal astronomisk klocka med gyllene detaljer i blått och rött mot mörk bakgrund",
|
||||||
|
category: "Stad och arkitektur",
|
||||||
|
},
|
||||||
|
"immich-50bce1ec-f162-4fa8-957c-4819689f70c4-20260905-DSC07617": {
|
||||||
|
alt: "Ett barn med blont hår håller en vuxens hand utomhus bredvid en grön gräsmatta",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"immich-50e9e443-c475-4da0-b438-1a6b3f740a8c-20250414-WJ9E0143-Enhanced-NR": {
|
||||||
|
alt: "Människor med röda paraplyer i en solig stad med blå och beige arkitektur",
|
||||||
|
category: "Stad och arkitektur",
|
||||||
|
},
|
||||||
|
"immich-54d22882-3bdc-4bd5-b4d5-7dd2e5d71656-20260905-DSC07825": {
|
||||||
|
alt: "En man i ljus skjorta håller ett barn i gröna kläder på en gräsmatta vid klippor",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"immich-590158f5-f459-4c16-b6dd-b4354c0ab512-20260824-DSC07025__2_": {
|
||||||
|
alt: "Närbild av ett hjortdjur med lurviga, basttäckta horn och mörk päls mot svart bakgrund",
|
||||||
|
category: "Natur och djur",
|
||||||
|
},
|
||||||
|
"immich-5a60699d-bf05-4c18-bb43-5969af8f0106-20260905-DSC07930": {
|
||||||
|
alt: "Ett barn i grön tröja leker med en grön vattenpump på en solig gräsmatta",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"immich-5f79107b-ffbe-4e32-8b9d-b37689b3352f-20260905-DSC07639": {
|
||||||
|
alt: "En man, en kvinna och två barn vid en träbyggnad med gula blommor utomhus",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"immich-61040612-f637-4bdb-893c-901171857f83-20260621-squareblur_1781332134108_13": {
|
||||||
|
alt: "Röd boj vid en vit träbom i stilla vatten med näckrosblad och vass i solnedgången",
|
||||||
|
category: "Natur och djur",
|
||||||
|
},
|
||||||
|
"immich-638a305e-2d06-4d4e-9166-5f3a80c906a2-20260905-DSC07710": {
|
||||||
|
alt: "Ett barn i blå tröja med brandbilar står i en grönskande miljö framför en man",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"immich-69aa5b22-f80d-4cb5-b022-e77132560985-20260606-_J9E6105": {
|
||||||
|
alt: "En orange och svart fjäril på vita blommor bland mörklila blad i starkt solljus",
|
||||||
|
category: "Natur och djur",
|
||||||
|
},
|
||||||
|
"immich-6ee56b75-ff51-4817-98c8-0fb3c4485931-20250415-WJ9E1163-Enhanced-NR": {
|
||||||
|
alt: "En stenbro full av människor över en flod mellan röda tak och torn",
|
||||||
|
category: "Stad och arkitektur",
|
||||||
|
},
|
||||||
|
"immich-6f8ae1a4-fc2d-478d-ba2e-8452db0a08f0-20240912-20240912-WJ9E9894": {
|
||||||
|
alt: "En liten bäck med mjukt vatten som flyter över mörka stenar och grönska",
|
||||||
|
category: "Natur och djur",
|
||||||
|
},
|
||||||
|
"immich-71cea45b-73c3-4e9a-9810-c0e070b4f5d0-20240914-20240914-WJ9E0284": {
|
||||||
|
alt: "En person i jeansjacka sedd bakifrån i ett solrosfält i solnedgångens varma ljus",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"immich-73a61049-65e8-47ed-9904-220bf316e351-20260605-_J9E5985": {
|
||||||
|
alt: "Närbild av en svartvit hund i en grön trädgård med suddiga rosa blommor i förgrunden",
|
||||||
|
category: "Natur och djur",
|
||||||
|
},
|
||||||
|
"immich-7c1fc70b-c766-4c97-ba30-7fbd321ac702-20260905-DSC07953": {
|
||||||
|
alt: "Ett barn med blont hår skrattar i en blå t-shirt med brandbilar framför trävägg",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"immich-7c32da7b-9a9a-4675-a813-4fa3d026bcf5-20260905-DSC07665": {
|
||||||
|
alt: "Ett leende barn i blå t-shirt i en dörröppning med suddiga lila blommor i förgrunden",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"immich-8113f930-24bf-4920-adc6-734e397bd3e5-20260905-DSC07800": {
|
||||||
|
alt: "Ett barn med blå hjälm på en röd cykel framför ett beige hus i dagsljus",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"immich-81d60de8-8a13-43e1-b549-ced73e326368-20250415-WJ9E1324": {
|
||||||
|
alt: "Mörkt fönster med utsikt mot en molnig himmel och skuggspel på en grå vägg",
|
||||||
|
category: "Stad och arkitektur",
|
||||||
|
},
|
||||||
|
"immich-827bde98-bbf1-4f1f-bd38-4ffffb8c6f03-20260920-DSC08725-2": {
|
||||||
|
alt: "En gulsvart humla på en mörkröd blomma med suddig grön och röd bakgrund",
|
||||||
|
category: "Makro och detaljer",
|
||||||
|
},
|
||||||
|
"immich-87fbfcab-81f8-4ccf-a80e-e097e9a0614d-20250415-WJ9E1448": {
|
||||||
|
alt: "En kvinna med vindpustat brunt hår och mörkblå jacka mot en bakgrund av vita blommor",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"immich-9ad43795-2df0-4ca4-8a69-f8f99954b7e8-20260605-_J9E5944": {
|
||||||
|
alt: "En svartvit hund med lockig päls sitter på grönt gräs bredvid en vit fotboll",
|
||||||
|
category: "Natur och djur",
|
||||||
|
},
|
||||||
|
"immich-a5086bb9-3868-442c-9715-f3d626ec0123-20260605-_J9E6018": {
|
||||||
|
alt: "Klara vattendroppar på ett mörkgrönt blad med tydliga ådror mot en svart bakgrund",
|
||||||
|
category: "Makro och detaljer",
|
||||||
|
},
|
||||||
|
"immich-a5129e40-445b-4fcf-a5c5-3a5202590f27-20260920-DSC08720-2": {
|
||||||
|
alt: "En gulsvart humla på en mörkröd blomma omgiven av gröna blad i dagsljus",
|
||||||
|
category: "Natur och djur",
|
||||||
|
},
|
||||||
|
"immich-a6fa98f0-ba73-42c2-91a6-07709e195015-20260905-DSC07869": {
|
||||||
|
alt: "Två barn sedda bakifrån bredvid en vuxen och en vit flätad korg i varmt ljus",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"immich-a71dc5cd-ede5-4ae2-94cb-07f06d074846-20260606-_J9E6073": {
|
||||||
|
alt: "Orange och svart fjäril på en ljusgrå träbit med blå färgfläckar mot suddig grön bakgrund",
|
||||||
|
category: "Natur och djur",
|
||||||
|
},
|
||||||
|
"immich-a8bd05fb-864a-4c87-8a6c-7cbfcc047d3d-20250415-WJ9E1249-Enhanced-NR": {
|
||||||
|
alt: "En person med långt brunt hår som blåser i vinden, sedd bakifrån i dämpat ljus",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"immich-ac8360da-9fbd-45e0-b740-d091356037fe-20241018-20241018-IMG_8978": {
|
||||||
|
alt: "Ett runt stentorn och en gul byggnad vid ett vatten med varma kvällsljus",
|
||||||
|
category: "Stad och arkitektur",
|
||||||
|
},
|
||||||
|
"immich-b241db80-28b8-4430-b08b-c3ce589a85a3-20260621-Kopia_av_IMG_8714": {
|
||||||
|
alt: "En rödbrun ekorre på en grov gren med suddiga gröna barr i bakgrunden",
|
||||||
|
category: "Natur och djur",
|
||||||
|
},
|
||||||
|
"immich-b2780899-4269-44be-8dd3-fed23d3e89d3-20260621-squareblur_1781332107423_9": {
|
||||||
|
alt: "En blå vintagebil på en mörk stadsgata med varma gula ljusreflektioner i asfalten",
|
||||||
|
category: "Stad och arkitektur",
|
||||||
|
},
|
||||||
|
"immich-b2f52935-d52a-46f1-9262-78a7b1e19a90-20260905-DSC07604": {
|
||||||
|
alt: "Ett barn i grön tröja och ljus keps framför en suddig röd bil",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"immich-b3c19299-0e5d-4261-a98a-1ed08f01a0a3-20260905-DSC07702": {
|
||||||
|
alt: "Ett barn i grön tröja sparkar på en fotboll på en gräsmatta framför ett staket",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"immich-b7ddab61-cbfc-4c5f-9354-b56bab0b38a1-20241018-20241018-IMG_8925-2": {
|
||||||
|
alt: "Orangea höstlöv hänger över mörkt vatten med tydliga speglingar och en stenvägg i bakgrunden",
|
||||||
|
category: "Natur och djur",
|
||||||
|
},
|
||||||
|
"immich-b99369cb-622e-4f31-9a9f-a33bc4e0030a-20260605-_J9E5916": {
|
||||||
|
alt: "En mörk hund med lockiga öron tittar uppåt mot en mörkgrön suddig bakgrund",
|
||||||
|
category: "Natur och djur",
|
||||||
|
},
|
||||||
|
"immich-bbf62d80-754f-4243-a71b-a5abe1e0340c-20260905-DSC07890": {
|
||||||
|
alt: "Ett barn i grön pikétröja pumpar vatten ur en gammal grön handpump i ett varmt motljus",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"immich-c3ab7ee4-8a84-42a4-a3e9-662469569b51-20260605-_J9E5888": {
|
||||||
|
alt: "En person håller om en svart och vit hund med lockig päls i dämpat inomhusljus",
|
||||||
|
category: "Natur och djur",
|
||||||
|
},
|
||||||
|
"immich-c6489154-53d1-47e2-8572-521dd851137a-20260905-DSC07823": {
|
||||||
|
alt: "En man i ljus skjorta lyfter ett barn på en gräsmatta bredvid en stor sten",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"immich-cbbd628e-bf5a-4a9e-85e0-d5229680f492-20260905-DSC07657": {
|
||||||
|
alt: "Ett barn i blå t-shirt står i dörröppningen till en vit träbyggnad med mörkt inre",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"immich-d058e797-861a-4cd3-86a1-63c616bf248f-20260905-DSC07711": {
|
||||||
|
alt: "Ett barn i blå tröja går mot en man i vit jacka i en grön trädgård",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"immich-d2d2eb01-597b-47ed-8055-f193a629c472-20260605-_J9E5860": {
|
||||||
|
alt: "En springande svartvit hund med fladdrande öron på en grön gräsmatta mot mörk bakgrund",
|
||||||
|
category: "Natur och djur",
|
||||||
|
},
|
||||||
|
"immich-d34b0499-d2c0-40fd-848a-8e06329b7bf5-20260905-DSC07607": {
|
||||||
|
alt: "En kvinna och ett barn går hand i hand framför ett hus i varmt ljus",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"immich-d8cb2d51-2ead-4cf0-9215-a59b07f26f6d-20260905-DSC07704": {
|
||||||
|
alt: "Ett barn i grön tröja och blå jeans sparkar en fotboll på gräs framför en stenvägg",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"immich-db3ffd50-7e6a-4319-8815-b388e52e9aa9-20260905-DSC07896": {
|
||||||
|
alt: "Ett barn i grön tröja pumpar vatten ur en mörk handpump på en grön gräsmatta",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"immich-ddb13056-990c-48dc-ab92-e03baadb7a44-20260905-DSC07647": {
|
||||||
|
alt: "En kvinna i blå kläder håller i händerna på ett barn utomhus på grön gräsmatta",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"immich-debf1a27-f337-4fc8-a556-c8b08ad98404-20260621-squareblur_1781332127640_12": {
|
||||||
|
alt: "En tom träbänk i en höstskog omgiven av gula och orangea löv",
|
||||||
|
category: "Natur och djur",
|
||||||
|
},
|
||||||
|
"immich-e20af65c-c05b-4585-81b4-dcad6b52e450-20240914-20240914-WJ9E0319": {
|
||||||
|
alt: "En person i motljus framför ett öppet landskap under en gyllene solnedgång",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"immich-e25d4130-b3d3-4c63-8c82-e0750efa41b8-20260621-squareblur_1781332028758_1": {
|
||||||
|
alt: "Mörka stentrappor med orangea löv som leder upp från en spegelblank vattenyta",
|
||||||
|
category: "Stad och arkitektur",
|
||||||
|
},
|
||||||
|
"immich-e3a7d5bc-9ee1-4d29-8673-65922736295f-20260920-DSC08813": {
|
||||||
|
alt: "En svartgul humla i flykt mot en lila blomma omgiven av mörkgröna blad",
|
||||||
|
category: "Makro och detaljer",
|
||||||
|
},
|
||||||
|
"immich-e7048f3d-da0d-432e-b833-8e766df5708b-20240914-20240914-WJ9E0285": {
|
||||||
|
alt: "En person med långt hår sedd bakifrån i ett solrosfält i varmt kvällsljus",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"immich-f1f02aac-7ef7-4cc0-818a-e06e0ecdf7f7-20241018-20241018-IMG_9028": {
|
||||||
|
alt: "En stenbro med valv som reflekteras i mörkt vatten under ett varmt gyllene ljus",
|
||||||
|
category: "Stad och arkitektur",
|
||||||
|
},
|
||||||
|
"immich-f2d03140-50d5-4e94-94e8-425fb605a110-20260905-DSC07904": {
|
||||||
|
alt: "Ett barn i grön tröja pumpar vatten ur en grön handpump på en gräsmatta",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"immich-f836d606-e331-4277-9500-b9906305544e-20260621-Kopia_av_IMG_8222": {
|
||||||
|
alt: "En rödbrun ekorre sitter på marken mot en grov grå trädstam i dagsljus",
|
||||||
|
category: "Natur och djur",
|
||||||
|
},
|
||||||
|
"immich-f8943f55-4cba-4521-b9a5-4e735f31a336-20241018-20241018-IMG_9057": {
|
||||||
|
alt: "Stadsmiljö i skymning med upplysta hus och färgglada ljusreflektioner i ett mörkt vatten",
|
||||||
|
category: "Stad och arkitektur",
|
||||||
|
},
|
||||||
|
"immich-f9dd008c-3445-4394-b4f4-329b0034fd2c-20241018-20241018-IMG_8996": {
|
||||||
|
alt: "En mörk stenbro över vatten med röda höstlöv och en byggnad i bakgrunden",
|
||||||
|
category: "Stad och arkitektur",
|
||||||
|
},
|
||||||
|
"immich-faee21c0-1609-4a35-aab5-0b5d68c4cab4-20260905-DSC07618": {
|
||||||
|
alt: "Ett leende barn i blå tröja håller en vuxens hand mot en grön bakgrund",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"immich-fbb8346a-5992-4136-a099-596c06bbde30-20260905-DSC07723": {
|
||||||
|
alt: "En man i ljus jacka och två barn springer över en grön gräsmatta under träd",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
"immich-fed1e776-2242-468a-9800-8a8901df2be0-20250413-WJ9E0284": {
|
||||||
|
alt: "En kvinna i blå jeansjacka med en kamera i en park med gula tulpaner",
|
||||||
|
category: "Porträtt och människor",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ const ogHeight = Number(og.attributes.height) || undefined;
|
|||||||
|
|
||||||
const nav = [
|
const nav = [
|
||||||
{ href: '/portfolio', label: 'Portfolio' },
|
{ href: '/portfolio', label: 'Portfolio' },
|
||||||
|
{ href: '/posters', label: 'Posters' },
|
||||||
{ href: '/tjanster', label: 'Tjänster' },
|
{ href: '/tjanster', label: 'Tjänster' },
|
||||||
{ href: '/about', label: 'Om mig' },
|
{ href: '/about', label: 'Om mig' },
|
||||||
{ href: '/contact', label: 'Kontakt' },
|
{ href: '/contact', label: 'Kontakt' },
|
||||||
|
|||||||
+8
-2
@@ -3,10 +3,16 @@
|
|||||||
// Nya sidor hamnar automatiskt i sitemap.
|
// Nya sidor hamnar automatiskt i sitemap.
|
||||||
const pageFiles = import.meta.glob('../pages/*.astro');
|
const pageFiles = import.meta.glob('../pages/*.astro');
|
||||||
|
|
||||||
|
// Sidor på samma domän som kundportalen (samma Cleura-server) visar, så att Om mig-texten,
|
||||||
|
// samarbeten och posters redigeras på ett ställe i admin (Linus 2026-11-06). Caddy skickar dem
|
||||||
|
// till portalen; enskilda posters listas i portalens egen /posters/sitemap.xml.
|
||||||
|
export const PORTAL_PAGES = ['/about', '/contact', '/posters'] as const;
|
||||||
|
|
||||||
export function sitePaths(): string[] {
|
export function sitePaths(): string[] {
|
||||||
return Object.keys(pageFiles)
|
const own = Object.keys(pageFiles)
|
||||||
.map((file) => file.replace(/^\.\.\/pages\//, '').replace(/\.astro$/, ''))
|
.map((file) => file.replace(/^\.\.\/pages\//, '').replace(/\.astro$/, ''))
|
||||||
.filter((name) => name !== '404' && !name.startsWith('_') && !name.startsWith('['))
|
.filter((name) => name !== '404' && !name.startsWith('_') && !name.startsWith('['))
|
||||||
.map((name) => (name === 'index' ? '/' : `/${name}`))
|
.map((name) => (name === 'index' ? '/' : `/${name}`));
|
||||||
|
return [...new Set([...own, ...PORTAL_PAGES])]
|
||||||
.sort((a, b) => (a === '/' ? -1 : b === '/' ? 1 : a.localeCompare(b)));
|
.sort((a, b) => (a === '/' ? -1 : b === '/' ? 1 : a.localeCompare(b)));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import WaveBackground from '../components/WaveBackground.astro';
|
|||||||
const links = [
|
const links = [
|
||||||
{ href: '/', label: 'Startsidan' },
|
{ href: '/', label: 'Startsidan' },
|
||||||
{ href: '/portfolio', label: 'Portfolio' },
|
{ href: '/portfolio', label: 'Portfolio' },
|
||||||
|
{ href: '/posters', label: 'Posters' },
|
||||||
{ href: '/tjanster', label: 'Tjänster och bokning' },
|
{ href: '/tjanster', label: 'Tjänster och bokning' },
|
||||||
{ href: '/contact', label: 'Kontakt' },
|
{ href: '/contact', label: 'Kontakt' },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,137 +0,0 @@
|
|||||||
---
|
|
||||||
// src/pages/about.astro
|
|
||||||
import MainLayout from '../layouts/MainLayout.astro';
|
|
||||||
import { Image } from 'astro:assets';
|
|
||||||
import profileImg from '../assets/images/about/profile.jpg';
|
|
||||||
import WaveBackground from '../components/WaveBackground.astro';
|
|
||||||
import { business, basedIn } from '../data/business';
|
|
||||||
---
|
|
||||||
|
|
||||||
<MainLayout
|
|
||||||
title="Om Linus Olofsson – fotograf | Images by Olofsson"
|
|
||||||
description={`Linus Olofsson är fotografen bakom Images by Olofsson, baserad i ${basedIn()}. ${business.recognitionThirdPerson} Läs om hans bildspråk och utrustning.`}
|
|
||||||
path="/about"
|
|
||||||
image={profileImg}
|
|
||||||
imageAlt="Porträtt av fotografen Linus Olofsson"
|
|
||||||
pageType="AboutPage"
|
|
||||||
breadcrumb="Om mig"
|
|
||||||
>
|
|
||||||
|
|
||||||
<WaveBackground opacity="opacity-80" />
|
|
||||||
|
|
||||||
<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">
|
|
||||||
|
|
||||||
<div class="flex flex-col lg:flex-row items-center gap-12 lg:gap-20">
|
|
||||||
|
|
||||||
<div class="w-full lg:w-1/2 relative group">
|
|
||||||
<div class="absolute inset-0 bg-gradient-to-tr from-neutral-900/50 to-transparent rounded-2xl z-10"></div>
|
|
||||||
<div class="relative rounded-2xl overflow-hidden shadow-2xl border border-white/10 transform transition duration-500 group-hover:scale-[1.01]">
|
|
||||||
<Image
|
|
||||||
src={profileImg}
|
|
||||||
alt="Porträtt av fotografen Linus Olofsson"
|
|
||||||
width={800}
|
|
||||||
height={1000}
|
|
||||||
class="w-full h-auto object-cover grayscale hover:grayscale-0 transition-all duration-700 ease-in-out"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w-full lg:w-1/2 space-y-6">
|
|
||||||
<h1 class="text-4xl md:text-5xl font-bold leading-tight">
|
|
||||||
Levande ögonblick, <br/>
|
|
||||||
<span class="text-transparent bg-clip-text bg-gradient-to-r from-white to-gray-400">fångade i hög upplösning.</span>
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div class="space-y-4 text-gray-300 text-lg leading-relaxed">
|
|
||||||
<p>
|
|
||||||
Hej, jag heter {business.person.name} och är fotografen bakom {business.brand}.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Jag är baserad i {basedIn()}. Allt började med en enkel nyfikenhet på hur ljuset formar sättet vi ser världen på. Med tiden blev nyfikenheten en besatthet av att fånga detaljerna som de flesta missar – texturer, strukturer, kontraster och ögonblick som passerar obemärkt förbi.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Jag hittar skönheten i det lilla: en sten, ett träd, en tom gata, en stilla skogsstig eller uttrycket hos en främling som går förbi.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Oavsett om det är de invecklade mönstren i en makrobild, naturens stora tystnad eller stadens energi är målet alltid detsamma – att skapa bilder som förmedlar känslor och berättar en historia utan ord.
|
|
||||||
</p>
|
|
||||||
<p>{business.recognition}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pt-4">
|
|
||||||
<p class="font-handwriting text-2xl text-white">Linus Olofsson</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-wrap gap-4 pt-6">
|
|
||||||
<a href="/contact" class="inline-block bg-white text-black px-8 py-3 rounded-full font-bold hover:bg-gray-200 transition-colors shadow-lg">
|
|
||||||
Kontakta mig
|
|
||||||
</a>
|
|
||||||
<a href="/portfolio" class="inline-block border border-white text-white px-8 py-3 rounded-full font-bold hover:bg-white/10 transition-colors">
|
|
||||||
Se världen genom mina ögon
|
|
||||||
</a>
|
|
||||||
<a href="/tjanster" class="inline-block border border-white/40 text-white px-8 py-3 rounded-full font-bold hover:bg-white/10 transition-colors">
|
|
||||||
Tjänster och bokning
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="border-t border-white/10 pt-16">
|
|
||||||
<h2 class="text-3xl font-bold mb-10 text-center text-white">Vad har jag i kameraväskan?</h2>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
|
|
||||||
|
|
||||||
<div data-trail-box class="gear-card">
|
|
||||||
<div class="icon-box">
|
|
||||||
<svg class="w-8 h-8 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
|
|
||||||
</div>
|
|
||||||
<h3 class="text-xl font-bold mb-2 text-white">Kamerahus</h3>
|
|
||||||
<p class="text-gray-300">{business.camera}</p>
|
|
||||||
<p class="text-gray-400 text-sm mt-2">En pålitlig följeslagare som har varit med mig på otaliga äventyr.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div data-trail-box class="gear-card">
|
|
||||||
<div class="icon-box">
|
|
||||||
<svg class="w-8 h-8 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" /></svg>
|
|
||||||
</div>
|
|
||||||
<h3 class="text-xl font-bold mb-2 text-white">Objektiv</h3>
|
|
||||||
<ul class="text-gray-300 text-sm space-y-1">
|
|
||||||
{business.lenses.map((lens) => <li>{lens}</li>)}
|
|
||||||
<li>och ett par extra batterier</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div data-trail-box class="gear-card">
|
|
||||||
<div class="icon-box">
|
|
||||||
<svg class="w-8 h-8 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" /></svg>
|
|
||||||
</div>
|
|
||||||
<h3 class="text-xl font-bold mb-2 text-white">Redigering</h3>
|
|
||||||
<p class="text-gray-300">{business.editing}</p>
|
|
||||||
<p class="text-gray-400 text-sm mt-2">Här får råfilerna liv. Jag fokuserar på naturliga toner och kontrast.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</MainLayout>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
@reference "../styles/global.css";
|
|
||||||
|
|
||||||
.font-handwriting {
|
|
||||||
font-family: 'Brush Script MT', cursive;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gear-card {
|
|
||||||
/* Jag la även till lite backdrop-blur på korten här nere för enhetlighet */
|
|
||||||
@apply bg-neutral-900/80 backdrop-blur-sm p-8 rounded-xl border border-white/5 hover:border-white/20 transition-all duration-300 flex flex-col items-center text-center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-box {
|
|
||||||
@apply w-16 h-16 bg-neutral-800 rounded-full flex items-center justify-center mb-6 text-white shadow-lg border border-white/5;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
---
|
|
||||||
// src/pages/contact.astro
|
|
||||||
import MainLayout from '../layouts/MainLayout.astro';
|
|
||||||
import WaveBackground from '../components/WaveBackground.astro';
|
|
||||||
import { business } from '../data/business';
|
|
||||||
---
|
|
||||||
|
|
||||||
<MainLayout
|
|
||||||
title="Kontakt och bokning | Images by Olofsson"
|
|
||||||
description={`Kontakta fotograf Linus Olofsson på ${business.email} eller Instagram ${business.social.instagram.handle}, eller skicka en bokningsförfrågan via kundportalen.`}
|
|
||||||
path="/contact"
|
|
||||||
pageType="ContactPage"
|
|
||||||
breadcrumb="Kontakt"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<WaveBackground opacity="opacity-80" theme="purple" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="container mx-auto px-4 py-12 md:py-20 relative z-10">
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-12 lg:gap-20 max-w-6xl mx-auto items-start">
|
|
||||||
|
|
||||||
<div class="space-y-8">
|
|
||||||
<div>
|
|
||||||
<h1 class="text-4xl md:text-5xl font-bold mb-6">Kontakta mig</h1>
|
|
||||||
<p class="text-gray-300 text-lg leading-relaxed">
|
|
||||||
Har du frågor om någon av mina bilder, vill du samarbeta eller bara säga hej?
|
|
||||||
Hör av dig via sociala medier eller mejla mig direkt.
|
|
||||||
</p>
|
|
||||||
<p class="text-gray-300 text-lg leading-relaxed mt-4">
|
|
||||||
Vill du boka en fotografering? Skicka en förfrågan i kundportalen så återkommer jag med en offert.
|
|
||||||
Läs mer om hur det går till under <a href="/tjanster" class="text-white underline underline-offset-4 hover:text-gray-300">Tjänster</a>.
|
|
||||||
</p>
|
|
||||||
<a href={business.portal.bookingUrl} class="inline-block mt-6 px-8 py-3 bg-white text-black font-bold rounded-full hover:bg-gray-200 transition">Skicka en bokningsförfrågan</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h2 class="text-white font-semibold text-xl mb-4">Du hittar mig också på</h2>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-3 gap-4">
|
|
||||||
|
|
||||||
<a href={business.social.instagram.url} target="_blank" rel="noopener noreferrer" class="social-card group" aria-label="Instagram">
|
|
||||||
<svg class="w-8 h-8 group-hover:text-pink-500 transition-colors" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"/></svg>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href={business.social.flickr} target="_blank" rel="noopener noreferrer" class="social-card group" aria-label="Flickr">
|
|
||||||
<svg class="w-8 h-8 group-hover:text-blue-500 transition-colors" viewBox="0 0 24 24" fill="currentColor"><path d="M24 4v16h-24v-16h24zm-16.146 11.908c1.652 0 2.992-1.339 2.992-2.991 0-1.652-1.34-2.991-2.992-2.991-1.651 0-2.99 1.339-2.99 2.991 0 1.652 1.339 2.991 2.99 2.991zm8.008 0c1.652 0 2.992-1.339 2.992-2.991 0-1.652-1.34-2.991-2.992-2.991-1.652 0-2.992 1.339-2.992 2.991 0 1.652 1.34 2.991 2.992 2.991z"/></svg>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href={business.social.lightroom} target="_blank" rel="noopener noreferrer" class="social-card group" aria-label="Adobe Lightroom">
|
|
||||||
<svg class="w-8 h-8 group-hover:text-cyan-400 transition-colors" viewBox="0 0 24 24" fill="currentColor">
|
|
||||||
<path d="M4 4v16h11v-4H8V4H4zm13 4c-2.21 0-4 1.79-4 4v8h4v-7c0-1.1.9-2 2-2v-3h-2z"/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href={business.social.github} target="_blank" rel="noopener noreferrer" class="social-card group" aria-label="GitHub">
|
|
||||||
<svg class="w-8 h-8 group-hover:text-purple-500 transition-colors" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href={business.social.linkedin} target="_blank" rel="noopener noreferrer" class="social-card group" aria-label="LinkedIn">
|
|
||||||
<svg class="w-8 h-8 group-hover:text-blue-400 transition-colors" viewBox="0 0 24 24" fill="currentColor"><path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z"/></svg>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href={`mailto:${business.email}`} class="social-card group" aria-label="E-post">
|
|
||||||
<svg class="w-8 h-8 group-hover:text-green-500 transition-colors" viewBox="0 0 24 24" fill="currentColor"><path d="M0 3v18h24v-18h-24zm6.623 7.929l-4.623 5.712v-11.174l4.623 5.462zm5.377 2.738l-5.354-6.324-3.791 3.214 9.145 10.829 9.145-10.829-3.791-3.214-5.354 6.324zm5.377-2.738l4.623-5.462v11.174l-4.623-5.712z"/></svg>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="relative group">
|
|
||||||
<div class="absolute -inset-1 bg-gradient-to-r from-purple-600 to-pink-600 rounded-2xl blur opacity-25 group-hover:opacity-50 transition duration-1000 group-hover:duration-200"></div>
|
|
||||||
|
|
||||||
<div data-trail-box class="relative bg-neutral-900 ring-1 ring-white/10 rounded-xl p-8 md:p-12 text-center space-y-10">
|
|
||||||
<div>
|
|
||||||
<h2 class="text-2xl font-bold text-white mb-2">Jag ser fram emot att höra av dig</h2>
|
|
||||||
<p class="text-gray-300">Hör gärna av dig med frågor, förfrågningar eller bara för att prata.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div class="inline-flex items-center justify-center p-4 bg-white/5 rounded-full mb-4 group-hover:scale-110 transition-transform duration-300">
|
|
||||||
<svg class="w-8 h-8 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-sm text-gray-400 uppercase tracking-widest mb-2">Mejla mig på</p>
|
|
||||||
<a href={`mailto:${business.email}`} class="text-xl md:text-3xl font-bold text-white hover:text-purple-400 transition-colors break-all">
|
|
||||||
{business.email}
|
|
||||||
</a>
|
|
||||||
<div>
|
|
||||||
<button type="button" id="email-btn" class="mt-3 text-xs uppercase tracking-widest text-gray-400 hover:text-white transition-colors" data-email={business.email}>Kopiera adressen</button>
|
|
||||||
</div>
|
|
||||||
<p id="copy-feedback" class="h-4 text-xs text-green-400 mt-2 opacity-0 transition-opacity" aria-live="polite">Kopierad!</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pt-2">
|
|
||||||
<p class="text-sm text-gray-400 uppercase tracking-widest mb-4">Eller skriv till mig på Instagram</p>
|
|
||||||
<a href={business.social.instagram.url} target="_blank" rel="noopener noreferrer" class="inline-flex items-center gap-2 text-xl font-bold text-white hover:text-pink-500 transition-colors">
|
|
||||||
{business.social.instagram.handle}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
const emailBtn = document.getElementById('email-btn');
|
|
||||||
const feedback = document.getElementById('copy-feedback');
|
|
||||||
|
|
||||||
if (emailBtn && feedback) {
|
|
||||||
emailBtn.addEventListener('click', () => {
|
|
||||||
const email = emailBtn.getAttribute('data-email');
|
|
||||||
if (email) {
|
|
||||||
navigator.clipboard.writeText(email).then(() => {
|
|
||||||
feedback.style.opacity = '1';
|
|
||||||
setTimeout(() => {
|
|
||||||
feedback.style.opacity = '0';
|
|
||||||
}, 2000);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</MainLayout>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
@reference "../styles/global.css";
|
|
||||||
|
|
||||||
.social-card {
|
|
||||||
@apply flex items-center justify-center p-4 rounded-xl bg-neutral-800 border border-white/5 text-gray-400 transition-all duration-300;
|
|
||||||
min-height: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-card:hover {
|
|
||||||
@apply bg-neutral-700 border-white/20 transform -translate-y-1 shadow-lg shadow-white/5;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -28,6 +28,7 @@ export const GET: APIRoute = ({ site }) => {
|
|||||||
business.phone ? `- Telefon: ${business.phone}` : null,
|
business.phone ? `- Telefon: ${business.phone}` : null,
|
||||||
`- E-post: ${business.email}`,
|
`- E-post: ${business.email}`,
|
||||||
`- Instagram: ${business.social.instagram.handle} (${business.social.instagram.url})`,
|
`- Instagram: ${business.social.instagram.handle} (${business.social.instagram.url})`,
|
||||||
|
`- Facebook: ${business.social.facebook}`,
|
||||||
`- Kamera: ${business.camera}`,
|
`- Kamera: ${business.camera}`,
|
||||||
`- Redigering: ${business.editing}`,
|
`- Redigering: ${business.editing}`,
|
||||||
`- Bokning: ${business.portal.bookingUrl}`,
|
`- Bokning: ${business.portal.bookingUrl}`,
|
||||||
@@ -39,7 +40,8 @@ export const GET: APIRoute = ({ site }) => {
|
|||||||
`- [Tjänster och vanliga frågor](${url('/tjanster')}): vad du kan boka, hur bokningen går till och svar på vanliga frågor`,
|
`- [Tjänster och vanliga frågor](${url('/tjanster')}): vad du kan boka, hur bokningen går till och svar på vanliga frågor`,
|
||||||
`- [Portfolio](${url('/portfolio')}): urval av fotografier`,
|
`- [Portfolio](${url('/portfolio')}): urval av fotografier`,
|
||||||
`- [Om mig](${url('/about')}): ${business.person.name}, bakgrund och utrustning`,
|
`- [Om mig](${url('/about')}): ${business.person.name}, bakgrund och utrustning`,
|
||||||
`- [Kontakt](${url('/contact')}): e-post, Instagram och bokning`,
|
`- [Posters](${url('/posters')}): fotografier som posters, välj storlek och material`,
|
||||||
|
`- [Kontakt och samarbeten](${url('/contact')}): samarbeten, reklam och sponsring`,
|
||||||
'',
|
'',
|
||||||
'## Vanliga frågor',
|
'## Vanliga frågor',
|
||||||
'',
|
'',
|
||||||
|
|||||||
+10
-152
@@ -90,23 +90,20 @@ const galleryItems = await Promise.all(Object.entries(imageFiles).map(async ([re
|
|||||||
].filter(Boolean).join(' ');
|
].filter(Boolean).join(' ');
|
||||||
const alt = meta.alt?.trim() || fallbackAlt.charAt(0).toUpperCase() + fallbackAlt.slice(1);
|
const alt = meta.alt?.trim() || fallbackAlt.charAt(0).toUpperCase() + fallbackAlt.slice(1);
|
||||||
|
|
||||||
// Nedladdning/lightbox: nya, EXIF-fria JPG:er i 800/1600/3000 px
|
// Lightbox: EXIF-fria JPG:er i 800/1600/3000 px (Astro hoppar över bredder större än originalet).
|
||||||
// (Astro hoppar över bredder större än originalet).
|
// Ingen nedladdning längre (Linus 2026-11-06): bilderna säljs som posters i stället.
|
||||||
const jpg = await getImage({ src: file.default, format: 'jpg', quality: 88, width: 800, widths: [800, 1600, 3000] });
|
const jpg = await getImage({ src: file.default, format: 'jpg', quality: 88, width: 800, widths: [800, 1600, 3000] });
|
||||||
const variants = [...jpg.srcSet.values].sort((a, b) => widthOf(a.descriptor ?? '') - widthOf(b.descriptor ?? ''));
|
const variants = [...jpg.srcSet.values].sort((a, b) => widthOf(a.descriptor ?? '') - widthOf(b.descriptor ?? ''));
|
||||||
const small = variants[0]?.url ?? jpg.src;
|
|
||||||
const large = variants[variants.length - 1]?.url ?? jpg.src;
|
const large = variants[variants.length - 1]?.url ?? jpg.src;
|
||||||
const medium = variants.find((v) => widthOf(v.descriptor ?? '') === 1600)?.url ?? large;
|
|
||||||
|
|
||||||
const assetId = key.match(/^immich-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})-/i)?.[1]?.toLowerCase() ?? null;
|
|
||||||
const statKey = ('portfolio/' + key.replace(/[^\w\-.]/g, '_')).slice(0, 150);
|
const statKey = ('portfolio/' + key.replace(/[^\w\-.]/g, '_')).slice(0, 150);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
img: file.default, key, alt, title: meta.title ?? null,
|
img: file.default, key, alt, title: meta.title ?? null,
|
||||||
year, month, monthIndex, fullDate, model: exifData?.Model,
|
year, month, monthIndex, fullDate, model: exifData?.Model,
|
||||||
fStop, shutterSpeed, iso, focalLength,
|
fStop, shutterSpeed, iso, focalLength,
|
||||||
dlS: small, dlM: medium, dlL: large, srcset: jpg.srcSet.attribute,
|
large, srcset: jpg.srcSet.attribute,
|
||||||
assetId, statKey,
|
statKey,
|
||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -157,17 +154,13 @@ const uniqueMonths = [...new Map(allMonths.map(item => [item.name, item])).value
|
|||||||
<div class="columns-1 md:columns-2 lg:columns-3 gap-6 space-y-6" id="gallery-grid">
|
<div class="columns-1 md:columns-2 lg:columns-3 gap-6 space-y-6" id="gallery-grid">
|
||||||
{galleryItems.map((item: any, index: number) => (
|
{galleryItems.map((item: any, index: number) => (
|
||||||
<a
|
<a
|
||||||
href={item.dlL}
|
href={item.large}
|
||||||
class="gallery-item block break-inside-avoid relative group overflow-hidden rounded-lg cursor-pointer bg-neutral-800 shadow-md"
|
class="gallery-item block break-inside-avoid relative group overflow-hidden rounded-lg cursor-pointer bg-neutral-800 shadow-md"
|
||||||
data-year={item.year}
|
data-year={item.year}
|
||||||
data-month={item.month}
|
data-month={item.month}
|
||||||
data-src={item.dlL}
|
data-src={item.large}
|
||||||
data-srcset={item.srcset}
|
data-srcset={item.srcset}
|
||||||
data-dl-s={item.dlS}
|
|
||||||
data-dl-m={item.dlM}
|
|
||||||
data-dl-l={item.dlL}
|
|
||||||
data-key={item.key}
|
data-key={item.key}
|
||||||
data-asset={item.assetId}
|
|
||||||
data-stat={item.statKey}
|
data-stat={item.statKey}
|
||||||
data-alt={item.alt}
|
data-alt={item.alt}
|
||||||
data-title={item.title}
|
data-title={item.title}
|
||||||
@@ -227,18 +220,9 @@ 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>
|
<p id="lb-model" class="text-gray-400 text-sm font-medium tracking-wide mt-1"></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap items-center gap-4 md:gap-6">
|
<div class="flex flex-wrap items-center gap-4 md:gap-6">
|
||||||
<div class="relative">
|
<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">
|
||||||
<button type="button" id="lb-download" aria-haspopup="true" aria-expanded="false" class="group 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 active:scale-95 active:duration-75">
|
Köp som poster
|
||||||
<svg id="lb-dl-arrow" xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 transition-colors duration-200" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4v12m0 0l-4-4m4 4l4-4M4 19h16"/></svg>
|
</a>
|
||||||
Ladda ner
|
|
||||||
</button>
|
|
||||||
<div id="lb-sizes" class="hidden absolute bottom-full mb-3 left-0 md:left-auto md:right-0 min-w-[260px] bg-neutral-900/95 backdrop-blur-md border border-white/10 rounded-xl shadow-2xl z-50 overflow-hidden origin-bottom opacity-0 translate-y-2 transition-all duration-200 ease-out">
|
|
||||||
<button type="button" data-size="s" class="dl-opt w-full text-left group flex items-center justify-between px-5 py-3.5 text-sm text-gray-200 hover:bg-black/40 hover:text-white cursor-pointer transition-colors border-b border-white/5"><span class="font-medium uppercase tracking-widest text-xs">Liten</span><span class="text-gray-400 text-xs font-mono group-hover:text-gray-300 transition-colors">800 px</span></button>
|
|
||||||
<button type="button" data-size="m" class="dl-opt w-full text-left group flex items-center justify-between px-5 py-3.5 text-sm text-gray-200 hover:bg-black/40 hover:text-white cursor-pointer transition-colors border-b border-white/5"><span class="font-medium uppercase tracking-widest text-xs">Mellan</span><span class="text-gray-400 text-xs font-mono group-hover:text-gray-300 transition-colors">1600 px</span></button>
|
|
||||||
<button type="button" data-size="l" class="dl-opt w-full text-left group flex items-center justify-between px-5 py-3.5 text-sm text-gray-200 hover:bg-black/40 hover:text-white cursor-pointer transition-colors border-b border-white/5"><span class="font-medium uppercase tracking-widest text-xs">Stor</span><span class="text-gray-400 text-xs font-mono group-hover:text-gray-300 transition-colors">3000 px</span></button>
|
|
||||||
<button type="button" data-size="orig" class="dl-opt w-full text-left group flex items-center justify-between px-5 py-3.5 text-sm text-gray-200 hover:bg-black/40 hover:text-white cursor-pointer transition-colors "><span class="font-medium uppercase tracking-widest text-xs">Original<span class="block text-[10px] text-gray-400 normal-case tracking-normal mt-0.5">Kräver lösenord</span></span><span class="text-gray-400 text-xs font-mono group-hover:text-gray-300 transition-colors">full storlek</span></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-wrap gap-3 md:gap-6 text-xs md:text-sm font-mono text-gray-300">
|
<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">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">Bländare</span><span id="lb-fstop">--</span></div>
|
||||||
@@ -249,21 +233,6 @@ const uniqueMonths = [...new Map(allMonths.map(item => [item.name, item])).value
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Lösenordsruta för ORIGINAL. Originalen ligger inte i repot (27–70 MB/st)
|
|
||||||
utan hämtas från Immich via /download/original, som kräver lösenord. -->
|
|
||||||
<div id="pw-modal" role="dialog" aria-modal="true" aria-labelledby="pw-title" class="hidden fixed inset-0 z-[60] bg-black/85 backdrop-blur-sm flex items-center justify-center p-4">
|
|
||||||
<div class="bg-neutral-900 border border-white/10 rounded-xl shadow-2xl p-7 w-full max-w-sm">
|
|
||||||
<p id="pw-title" class="text-white font-bold text-lg tracking-tight mb-1">Ladda ner original</p>
|
|
||||||
<p class="text-gray-400 text-sm mb-4">Originalfilen är i full upplösning och kräver lösenord.</p>
|
|
||||||
<input id="pw-input" type="password" aria-label="Lösenord" placeholder="Lösenord" autocomplete="current-password"
|
|
||||||
class="w-full bg-black/60 border border-white/20 rounded-lg text-white px-4 py-2.5 mb-2 focus:outline-none focus:border-white/60 focus:ring-1 focus:ring-white/20 transition" />
|
|
||||||
<p id="pw-error" class="hidden text-red-400 text-xs tracking-wide mb-2">Fel lösenord — försök igen.</p>
|
|
||||||
<div class="flex gap-2 justify-end mt-2">
|
|
||||||
<button type="button" id="pw-cancel" class="px-5 py-2.5 text-xs uppercase tracking-widest text-gray-400 hover:text-white rounded-full transition-colors">Avbryt</button>
|
|
||||||
<button type="button" id="pw-ok" class="px-5 py-2.5 text-xs uppercase tracking-widest bg-white text-black font-bold rounded-full hover:bg-gray-200 transition transform hover:scale-105">Ladda ner</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@@ -276,7 +245,7 @@ const uniqueMonths = [...new Map(allMonths.map(item => [item.name, item])).value
|
|||||||
function statsItem(item?: HTMLElement): string {
|
function statsItem(item?: HTMLElement): string {
|
||||||
return item?.dataset.stat || 'portfolio/okand';
|
return item?.dataset.stat || 'portfolio/okand';
|
||||||
}
|
}
|
||||||
function stat(kind: 'view' | 'open' | 'download', item: string) {
|
function stat(kind: 'view' | 'open', item: string) {
|
||||||
try {
|
try {
|
||||||
const body = new Blob([JSON.stringify({ kind, item })], { type: 'text/plain' });
|
const body = new Blob([JSON.stringify({ kind, item })], { type: 'text/plain' });
|
||||||
navigator.sendBeacon?.(STATS_URL, body);
|
navigator.sendBeacon?.(STATS_URL, body);
|
||||||
@@ -348,115 +317,6 @@ const uniqueMonths = [...new Map(allMonths.map(item => [item.name, item])).value
|
|||||||
document.body.style.overflow = 'hidden';
|
document.body.style.overflow = 'hidden';
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- NEDLADDNING (Small/Medium/Large ur repot, Original via proxy) ---
|
|
||||||
const dlBtn = document.getElementById('lb-download');
|
|
||||||
const dlMenu = document.getElementById('lb-sizes');
|
|
||||||
const pwModal = document.getElementById('pw-modal');
|
|
||||||
const pwInput = document.getElementById('pw-input') as HTMLInputElement;
|
|
||||||
const pwError = document.getElementById('pw-error');
|
|
||||||
|
|
||||||
// Small/Medium/Large är EXIF-fria JPG:er som Astro tar fram vid bygget
|
|
||||||
// (data-dl-s/m/l). Original hämtas från Immich via proxyn och kräver lösenord.
|
|
||||||
function urlForSize(size: string): string | null {
|
|
||||||
const item = visibleItems[currentImageIndex];
|
|
||||||
if (!item) return null;
|
|
||||||
if (size === 's') return item.dataset.dlS || null;
|
|
||||||
if (size === 'm') return item.dataset.dlM || null;
|
|
||||||
if (size === 'l') return item.dataset.dlL || null;
|
|
||||||
if (size === 'orig') return item.dataset.asset ? `/download/original?asset=${item.dataset.asset}` : null;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fileNameFor(size: string): string {
|
|
||||||
const item = visibleItems[currentImageIndex];
|
|
||||||
const key = (item?.dataset.key || 'bild').replace(/[^\w\-]/g, '_');
|
|
||||||
return `imagesbyolofsson-${key}-${size}.jpg`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function startDownload(url: string, filnamn?: string) {
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = filnamn || '';
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
a.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
const dlArrow = document.getElementById('lb-dl-arrow');
|
|
||||||
|
|
||||||
function visaMeny(visa: boolean) {
|
|
||||||
if (!dlMenu) return;
|
|
||||||
// Pilen går grå medan menyn är öppen och tillbaka till vanligt när den stängs
|
|
||||||
// (både vid val av storlek och när man klickar bort menyn).
|
|
||||||
dlArrow?.classList.toggle('text-gray-500', visa);
|
|
||||||
dlBtn?.setAttribute('aria-expanded', visa ? 'true' : 'false');
|
|
||||||
if (visa) {
|
|
||||||
dlMenu.classList.remove('hidden');
|
|
||||||
requestAnimationFrame(() => dlMenu.classList.remove('opacity-0', 'translate-y-2'));
|
|
||||||
} else {
|
|
||||||
dlMenu.classList.add('opacity-0', 'translate-y-2');
|
|
||||||
setTimeout(() => dlMenu.classList.add('hidden'), 180);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dlBtn?.addEventListener('click', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
visaMeny(dlMenu?.classList.contains('hidden') ?? true);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener('click', (e) => {
|
|
||||||
if (!dlMenu || dlMenu.classList.contains('hidden')) return;
|
|
||||||
if (!dlMenu.contains(e.target as Node) && !dlBtn?.contains(e.target as Node)) visaMeny(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelectorAll('.dl-opt').forEach((opt) => {
|
|
||||||
opt.addEventListener('click', () => {
|
|
||||||
const size = (opt as HTMLElement).dataset.size || 'l';
|
|
||||||
visaMeny(false);
|
|
||||||
const url = urlForSize(size);
|
|
||||||
if (!url) { alert('Den storleken saknas för den här bilden.'); return; }
|
|
||||||
stat('download', statsItem(visibleItems[currentImageIndex]));
|
|
||||||
if (size === 'orig') {
|
|
||||||
// Lösenordet sparas bara för fliken, aldrig i localStorage.
|
|
||||||
const sparat = sessionStorage.getItem('dlpw');
|
|
||||||
if (sparat) { startDownload(`${url}&pw=${encodeURIComponent(sparat)}`); return; }
|
|
||||||
pwError?.classList.add('hidden');
|
|
||||||
if (pwInput) pwInput.value = '';
|
|
||||||
pwModal?.classList.remove('hidden');
|
|
||||||
setTimeout(() => pwInput?.focus(), 50);
|
|
||||||
} else {
|
|
||||||
startDownload(url, fileNameFor(size));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
async function forsokOriginal() {
|
|
||||||
const pw = pwInput?.value || '';
|
|
||||||
if (!pw) return;
|
|
||||||
const url = urlForSize('orig');
|
|
||||||
if (!url) return;
|
|
||||||
// HEAD först: då får vi 401 utan att dra igång en 70 MB-nedladdning.
|
|
||||||
try {
|
|
||||||
const r = await fetch(`${url}&pw=${encodeURIComponent(pw)}`, { method: 'HEAD' });
|
|
||||||
if (r.status === 401) { pwError?.classList.remove('hidden'); return; }
|
|
||||||
if (!r.ok) { alert('Kunde inte hämta originalet just nu.'); return; }
|
|
||||||
} catch { /* nätverksfel -> försök ändå nedan */ }
|
|
||||||
sessionStorage.setItem('dlpw', pw);
|
|
||||||
pwModal?.classList.add('hidden');
|
|
||||||
startDownload(`${url}&pw=${encodeURIComponent(pw)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('pw-ok')?.addEventListener('click', forsokOriginal);
|
|
||||||
pwInput?.addEventListener('keydown', (e) => { if ((e as KeyboardEvent).key === 'Enter') forsokOriginal(); });
|
|
||||||
document.getElementById('pw-cancel')?.addEventListener('click', () => pwModal?.classList.add('hidden'));
|
|
||||||
|
|
||||||
document.addEventListener('keydown', (e) => {
|
|
||||||
if ((e as KeyboardEvent).key === 'Escape' && !dlMenu?.classList.contains('hidden')) {
|
|
||||||
e.stopImmediatePropagation();
|
|
||||||
visaMeny(false);
|
|
||||||
}
|
|
||||||
}, true);
|
|
||||||
|
|
||||||
function closeLightbox() {
|
function closeLightbox() {
|
||||||
lightbox?.classList.add('opacity-0');
|
lightbox?.classList.add('opacity-0');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -468,8 +328,6 @@ const uniqueMonths = [...new Map(allMonths.map(item => [item.name, item])).value
|
|||||||
function updateLightboxContent() {
|
function updateLightboxContent() {
|
||||||
const item = visibleItems[currentImageIndex];
|
const item = visibleItems[currentImageIndex];
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
visaMeny(false);
|
|
||||||
|
|
||||||
// srcset först så att webbläsaren väljer 800/1600/3000 px efter skärmen.
|
// srcset först så att webbläsaren väljer 800/1600/3000 px efter skärmen.
|
||||||
lbImg.srcset = item.dataset.srcset || '';
|
lbImg.srcset = item.dataset.srcset || '';
|
||||||
lbImg.src = item.dataset.src || '';
|
lbImg.src = item.dataset.src || '';
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ const link = 'text-white underline hover:text-gray-300';
|
|||||||
<ul class="list-disc pl-6 space-y-2">
|
<ul class="list-disc pl-6 space-y-2">
|
||||||
<li>Det finns inga användarkonton och inga kontaktformulär på den här webbplatsen. Bokningar görs i den separata kundportalen.</li>
|
<li>Det finns inga användarkonton och inga kontaktformulär på den här webbplatsen. Bokningar görs i den separata kundportalen.</li>
|
||||||
<li>Om du mejlar mig används din adress och ditt meddelande bara för att svara dig och delas inte med någon.</li>
|
<li>Om du mejlar mig används din adress och ditt meddelande bara för att svara dig och delas inte med någon.</li>
|
||||||
<li>Lösenordsskyddade nedladdningar sparar lösenordet bara i din öppna webbläsarflik; det finns inte kvar när du stänger den.</li>
|
|
||||||
<li>
|
<li>
|
||||||
Webbplatsen levereras via Cloudflare, som behandlar tekniska uppgifter som din IP-adress för att leverera
|
Webbplatsen levereras via Cloudflare, som behandlar tekniska uppgifter som din IP-adress för att leverera
|
||||||
och skydda webbplatsen. Se <a href="https://www.cloudflare.com/privacypolicy/" target="_blank" rel="noopener noreferrer" class={link}>Cloudflares integritetspolicy</a>.
|
och skydda webbplatsen. Se <a href="https://www.cloudflare.com/privacypolicy/" target="_blank" rel="noopener noreferrer" class={link}>Cloudflares integritetspolicy</a>.
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export const GET: APIRoute = ({ site }) => {
|
|||||||
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
<sitemap><loc>${new URL('/sitemap-0.xml', base).toString()}</loc></sitemap>
|
<sitemap><loc>${new URL('/sitemap-0.xml', base).toString()}</loc></sitemap>
|
||||||
|
<sitemap><loc>${new URL('/posters/sitemap.xml', base).toString()}</loc></sitemap>
|
||||||
</sitemapindex>
|
</sitemapindex>
|
||||||
`;
|
`;
|
||||||
return new Response(body, { headers: { 'Content-Type': 'application/xml; charset=utf-8' } });
|
return new Response(body, { headers: { 'Content-Type': 'application/xml; charset=utf-8' } });
|
||||||
|
|||||||
Reference in New Issue
Block a user