Initial commit: Modern photography portfolio with Astro

- Built with Astro 5.16.7 and Tailwind CSS 4.1.18
- Features advanced animations (waves, 3D scroll, smoke effects)
- Fully responsive design with mobile-first approach
- SEO optimized with semantic HTML
- Interactive elements and smooth transitions
- Professional portfolio showcase
This commit is contained in:
Linus Olofsson
2026-01-28 00:05:23 +01:00
commit b0b633d32b
84 changed files with 1656 additions and 0 deletions
+107
View File
@@ -0,0 +1,107 @@
# Dependencies
node_modules/
package-lock.json
# Build outputs
dist/
.build/
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# IDE & Editor files
.vscode/
.idea/
*.swp
*.swo
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
*.lcov
# nyc test coverage
.nyc_output
# Temporary folders
tmp/
temp/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Storybook build outputs
.out
.storybook-out
# Temporary folders
.tmp/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+160
View File
@@ -0,0 +1,160 @@
# Images by Olofsson
En modern, responsiv fotografportfolio webbplats byggd med Astro, Tailwind CSS och avancerade animationer.
[![Live Demo](https://img.shields.io/badge/Live-Demo-brightgreen?style=for-the-badge)](https://imagesbyolofsson.se)
[![Astro](https://img.shields.io/badge/Astro-5.16.7-FF5E01?style=for-the-badge&logo=astro)](https://astro.build)
[![Tailwind CSS](https://img.shields.io/badge/Tailwind_CSS-4.1.18-06B6D4?style=for-the-badge&logo=tailwindcss)](https://tailwindcss.com)
## Funktioner
- **Modern Design** - Minimalistisk och elegant design med fokus på bilder
- **Avancerade Animationer** - Våg-animationer, 3D-scroll effekter och rök-partiklar
- **Fullt Responsiv** - Perfekt anpassad för alla enheter
- **Blazingly Fast** - Byggd med Astro för maximal prestanda
- **Interaktiva Element** - Hover-effekter, smooth transitions och dynamiska bakgrunder
- **SEO-optimerad** - Semantisk HTML och metadata
## Bildexempel
Här är några exempel på hur bilder presenteras på sidan:
### Portfolio Grid
![Portfolio Grid](https://imagesbyolofsson.se/images/portfolio-grid.jpg)
*Ett snyggt rutnät av bilder med hover-effekter*
### 3D Scroll Effect
![3D Scroll](https://imagesbyolofsson.se/images/3d-scroll-effect.jpg)
*Dynamisk 3D-parallax effekt vid scrollning*
### Hero Section
![Hero Section](https://imagesbyolofsson.se/images/hero-section.jpg)
*Imponerande hero-sektion med animerad text och bakgrunder*
## Snabbstart
### Förutsättningar
- Node.js 18.0+
- npm eller yarn
### Installation
```bash
# Klona repot
git clone https://github.com/LinusOlofsson-maker/imagesbyolofsson.se.git
cd imagesbyolofsson.se
# Installera dependencies
npm install
# Starta utvecklingsservern
npm run dev
```
Öppna [http://localhost:4321](http://localhost:4321) i din webbläsare.
### Bygg för produktion
```bash
# Bygg projektet
npm run build
# Förhandsgranska production build
npm run preview
```
## Projektstruktur
```
src/
├── assets/ # Statiska resurser (bilder, SVG:er)
├── components/ # Återanvändbara Astro-komponenter
│ ├── WaveBackground.astro # Animerad vågbakgrund
│ ├── ThreeDScroll.astro # 3D scroll-effekt
│ ├── FloatingOrbs.astro # Flytande orb-element
│ └── Welcome.astro # Välkomstkomponent
├── layouts/ # Sid-layouter
│ ├── MainLayout.astro # Huvudlayout
│ └── Layout.astro # Bas-layout
├── pages/ # Sid-komponenter
│ ├── index.astro # Startsida
│ ├── portfolio.astro # Portfolio-sida
│ ├── about.astro # Om-sida
│ └── contact.astro # Kontakt-sida
└── styles/ # CSS-stilar
└── global.css # Globala stilar
```
## Teknologier
- **[Astro](https://astro.build/)** - Modern webb-ramverk
- **[Tailwind CSS](https://tailwindcss.com/)** - Utility-first CSS framework
- **[TypeScript](https://www.typescriptlang.org/)** - Typsäker JavaScript
- **[Exifr](https://github.com/mattiasw/ExifReader)** - EXIF-data läsning
## Designsystem
### Färgpalett
- **Primär**: Cyan (#06B6D4)
- **Sekundär**: Lila (#A855F7)
- **Neutral**: Gråskalor (#1F2937 - #F9FAFB)
### Typografi
- **Rubriker**: Font Black, Tracking Tighter
- **Brödtext**: Font Light
- **Accent**: Uppercase, Tracking Widest
### Animationer
- **Vågor**: SVG-baserade med filter-effekter
- **Rök**: Path-animationer med gaussian blur
- **3D Scroll**: Parallax med transform3d
- **Transitions**: Smooth ease-in/out
## Responsiv Design
Webbplatsen är optimerad för:
- **Mobil** (< 768px) - Enkel kolumn, touch-vänlig
- **Tablet** (768px - 1024px) - Mellanstor layout
- **Desktop** (> 1024px) - Full upplevelse med alla animationer
## Prestanda
- **Lighthouse Score**: 95+ Performance
- **First Contentful Paint**: < 1.5s
- **Largest Contentful Paint**: < 2.5s
- **Cumulative Layout Shift**: < 0.1
## Bidrag
Alla bidrag är välkomna! Vänligen följ dessa steg:
1. Forka detta repo
2. Skapa en feature branch (`git checkout -b feature/AmazingFeature`)
3. Committa dina ändringar (`git commit -m 'Add some AmazingFeature'`)
4. Pusha till branchen (`git push origin feature/AmazingFeature`)
5. Öppna en Pull Request
## Licens
Detta projekt är licensierat under MIT License - se [LICENSE](LICENSE) filen för detaljer.
## Kontakt
- **Webbplats**: [imagesbyolofsson.se](https://imagesbyolofsson.se)
- **Email**: info@imagesbyolofsson.se
- **Instagram**: [@imagesbyolofsson](https://www.instagram.com/imagesbyolofsson/)
- **LinkedIn**: [Linus Olofsson](https://www.linkedin.com/in/linus-olofsson-/)
## Tack
Tack till alla fantastiska open-source projekt som gjort detta möjligt:
- [Astro](https://astro.build/) - För det fantastiska webb-ramverket
- [Tailwind CSS](https://tailwindcss.com/) - För det kraftfulla CSS-ramverket
- [Adobe Lightroom](https://lightroom.adobe.com/) - För bildbehandling
---
<div align="center">
<p>Byggd av <a href="https://github.com/LinusOlofsson-maker">Linus Olofsson</a></p>
<p>© 2024 Images by Olofsson. Alla rättigheter reserverade.</p>
</div>
+20
View File
@@ -0,0 +1,20 @@
{
"name": "imagesbyolofsson",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.18",
"astro": "^5.16.7",
"exifr": "^7.1.3",
"tailwindcss": "^4.1.18"
},
"devDependencies": {
"@types/node": "^25.0.3"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 700 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 542 KiB

+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
<style>
path { fill: #000; }
@media (prefers-color-scheme: dark) {
path { fill: #FFF; }
}
</style>
</svg>

After

Width:  |  Height:  |  Size: 749 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width="115" height="48"><path fill="#17191E" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="url(#a)" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="#17191E" d="M.02 30.31s4.02-1.95 8.05-1.95l3.04-9.4c.11-.45.44-.76.82-.76.37 0 .7.31.82.76l3.04 9.4c4.77 0 8.05 1.95 8.05 1.95L17 11.71c-.2-.56-.53-.91-.98-.91H7.83c-.44 0-.76.35-.97.9L.02 30.31Zm42.37-5.97c0 1.64-2.05 2.62-4.88 2.62-1.85 0-2.5-.45-2.5-1.41 0-1 .8-1.49 2.65-1.49 1.67 0 3.09.03 4.73.23v.05Zm.03-2.04a21.37 21.37 0 0 0-4.37-.36c-5.32 0-7.82 1.25-7.82 4.18 0 3.04 1.71 4.2 5.68 4.2 3.35 0 5.63-.84 6.46-2.92h.14c-.03.5-.05 1-.05 1.4 0 1.07.18 1.16 1.06 1.16h4.15a16.9 16.9 0 0 1-.36-4c0-1.67.06-2.93.06-4.62 0-3.45-2.07-5.64-8.56-5.64-2.8 0-5.9.48-8.26 1.19.22.93.54 2.83.7 4.06 2.04-.96 4.95-1.37 7.2-1.37 3.11 0 3.97.71 3.97 2.15v.57Zm11.37 3c-.56.07-1.33.07-2.12.07-.83 0-1.6-.03-2.12-.1l-.02.58c0 2.85 1.87 4.52 8.45 4.52 6.2 0 8.2-1.64 8.2-4.55 0-2.74-1.33-4.09-7.2-4.39-4.58-.2-4.99-.7-4.99-1.28 0-.66.59-1 3.65-1 3.18 0 4.03.43 4.03 1.35v.2a46.13 46.13 0 0 1 4.24.03l.02-.55c0-3.36-2.8-4.46-8.2-4.46-6.08 0-8.13 1.49-8.13 4.39 0 2.6 1.64 4.23 7.48 4.48 4.3.14 4.77.62 4.77 1.28 0 .7-.7 1.03-3.71 1.03-3.47 0-4.35-.48-4.35-1.47v-.13Zm19.82-12.05a17.5 17.5 0 0 1-6.24 3.48c.03.84.03 2.4.03 3.24l1.5.02c-.02 1.63-.04 3.6-.04 4.9 0 3.04 1.6 5.32 6.58 5.32 2.1 0 3.5-.23 5.23-.6a43.77 43.77 0 0 1-.46-4.13c-1.03.34-2.34.53-3.78.53-2 0-2.82-.55-2.82-2.13 0-1.37 0-2.65.03-3.84 2.57.02 5.13.07 6.64.11-.02-1.18.03-2.9.1-4.04-2.2.04-4.65.07-6.68.07l.07-2.93h-.16Zm13.46 6.04a767.33 767.33 0 0 1 .07-3.18H82.6c.07 1.96.07 3.98.07 6.92 0 2.95-.03 4.99-.07 6.93h5.18c-.09-1.37-.11-3.68-.11-5.65 0-3.1 1.26-4 4.12-4 1.33 0 2.28.16 3.1.46.03-1.16.26-3.43.4-4.43-.86-.25-1.81-.41-2.96-.41-2.46-.03-4.26.98-5.1 3.38l-.17-.02Zm22.55 3.65c0 2.5-1.8 3.66-4.64 3.66-2.81 0-4.61-1.1-4.61-3.66s1.82-3.52 4.61-3.52c2.82 0 4.64 1.03 4.64 3.52Zm4.71-.11c0-4.96-3.87-7.18-9.35-7.18-5.5 0-9.23 2.22-9.23 7.18 0 4.94 3.49 7.59 9.21 7.59 5.77 0 9.37-2.65 9.37-7.6Z"/><defs><linearGradient id="a" x1="6.33" x2="19.43" y1="40.8" y2="34.6" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="1024" fill="none"><path fill="url(#a)" fill-rule="evenodd" d="M-217.58 475.75c91.82-72.02 225.52-29.38 341.2-44.74C240 415.56 372.33 315.14 466.77 384.9c102.9 76.02 44.74 246.76 90.31 366.31 29.83 78.24 90.48 136.14 129.48 210.23 57.92 109.99 169.67 208.23 155.9 331.77-13.52 121.26-103.42 264.33-224.23 281.37-141.96 20.03-232.72-220.96-374.06-196.99-151.7 25.73-172.68 330.24-325.85 315.72-128.6-12.2-110.9-230.73-128.15-358.76-12.16-90.14 65.87-176.25 44.1-264.57-26.42-107.2-167.12-163.46-176.72-273.45-10.15-116.29 33.01-248.75 124.87-320.79Z" clip-rule="evenodd" style="opacity:.154"/><path fill="url(#b)" fill-rule="evenodd" d="M1103.43 115.43c146.42-19.45 275.33-155.84 413.5-103.59 188.09 71.13 409 212.64 407.06 413.88-1.94 201.25-259.28 278.6-414.96 405.96-130 106.35-240.24 294.39-405.6 265.3-163.7-28.8-161.93-274.12-284.34-386.66-134.95-124.06-436-101.46-445.82-284.6-9.68-180.38 247.41-246.3 413.54-316.9 101.01-42.93 207.83 21.06 316.62 6.61Z" clip-rule="evenodd" style="opacity:.154"/><defs><linearGradient id="b" x1="373" x2="1995.44" y1="1100" y2="118.03" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient><linearGradient id="a" x1="107.37" x2="1130.66" y1="1993.35" y2="1026.31" gradientUnits="userSpaceOnUse"><stop stop-color="#3245FF"/><stop offset="1" stop-color="#BC52EE"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 700 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 542 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 417 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 761 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 716 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 964 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 619 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 433 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 639 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 817 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1012 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 676 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 865 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 916 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 949 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 838 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 885 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 765 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 561 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1009 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 978 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

+76
View File
@@ -0,0 +1,76 @@
---
// src/components/FloatingOrbs.astro
// Vi skapar en array med 8 "orbs" (du kan ändra antalet här)
const orbs = Array.from({ length: 8 });
---
<div class="fixed inset-0 pointer-events-none overflow-hidden z-0">
{orbs.map((_, i) => {
// Generera slumpmässiga värden för varje orb för variation
const top = Math.random() * 100; // Position Y (0-100%)
const left = Math.random() * 100; // Position X (0-100%)
// Storlek mellan ca 80px och 200px
const size = 80 + Math.random() * 120;
// Animationstid mellan 15s och 35s (långsamt)
const duration = 15 + Math.random() * 20;
// Negativ delay så de inte startar samtidigt
const delay = Math.random() * -30;
// Slumpa mellan två animationstyper för mer liv
const animName = Math.random() > 0.5 ? 'float-1' : 'float-2';
return (
<div class="orb" style={{
'--top': `${top}%`,
'--left': `${left}%`,
'--size': `${size}px`,
'--duration': `${duration}s`,
'--delay': `${delay}s`,
'--anim-name': animName
}}></div>
);
})}
</div>
<style>
/* Referera till globala styles för att komma åt Tailwind-färger */
@reference "../styles/global.css";
.orb {
position: absolute;
top: var(--top);
left: var(--left);
width: var(--size);
height: var(--size);
border-radius: 50%;
/* Färgen: Använder cyan-400 från Tailwind med låg opacitet */
background-color: theme('colors.cyan.400 / 0.2');
/* Den viktigaste delen för "spök-looken": kraftig blur och glow */
filter: blur(30px);
box-shadow: 0 0 60px theme('colors.cyan.400 / 0.4');
/* Animationen */
animation: var(--anim-name) var(--duration) ease-in-out infinite alternate var(--delay);
/* Gör att de inte blockerar musklick */
pointer-events: none;
/* Blandningsläge för snyggare effekt mot mörk bakgrund */
mix-blend-mode: screen;
}
/* Animation 1: Rör sig lite uppåt och vänster */
@keyframes float-1 {
0% { transform: translate(0, 0) scale(1); opacity: 0.4; }
100% { transform: translate(-50px, -30px) scale(1.1); opacity: 0.7; }
}
/* Animation 2: Rör sig lite nedåt och höger */
@keyframes float-2 {
0% { transform: translate(0, 0) scale(1); opacity: 0.4; }
100% { transform: translate(40px, 40px) scale(1.2); opacity: 0.6; }
}
</style>
+102
View File
@@ -0,0 +1,102 @@
---
// src/components/ThreeDScroll.astro
const images = [
{ src: "/3d-scroll/img1.jpg", alt: "Architecture Dark" },
{ src: "/3d-scroll/img2.jpg", alt: "Shadows Light" },
{ src: "/3d-scroll/img3.jpg", alt: "Portrait High End" },
{ src: "/3d-scroll/img4.jpg", alt: "Minimalist Landscape" },
{ src: "/3d-scroll/img5.jpg", alt: "Foggy Forest" },
];
---
<div id="scroll-container" class="relative h-[400vh] w-full">
<div class="sticky top-0 h-screen w-full overflow-hidden perspective-container bg-transparent">
<div id="scene" class="absolute inset-0 w-full h-full transform-style-3d flex items-center justify-center">
{images.map((img, index) => (
<div
class="gallery-item absolute transform-style-3d will-change-transform flex items-center justify-center"
data-index={index}
>
<img
src={img.src}
alt={img.alt}
class="floating-animation max-w-[80vw] max-h-[70vh] w-auto h-auto object-contain rounded-lg shadow-[0_20px_60px_-15px_rgba(0,0,0,0.8)] border border-white/5 grayscale hover:grayscale-0 transition-all duration-700 ease-out"
/>
</div>
))}
</div>
</div>
</div>
<style>
.perspective-container {
perspective: 1000px;
}
.transform-style-3d {
transform-style: preserve-3d;
}
/* Svävande animation */
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-5px); }
}
/* Vi sätter olika delay på animationen så alla bilder inte svävar i synk */
.floating-animation {
animation: float 6s ease-in-out infinite;
}
.gallery-item:nth-child(odd) .floating-animation {
animation-duration: 7s;
animation-delay: 1s;
}
</style>
<script>
const container = document.getElementById('scroll-container');
const scene = document.getElementById('scene');
const items = document.querySelectorAll('.gallery-item');
const Z_SPACING = 1500;
const START_Z = -500;
function updateScroll() {
if (!container || !scene) return;
const rect = container.getBoundingClientRect();
const scrollProgress = -rect.top;
// Om vi inte scrollat in än, nollställ
if (scrollProgress < 0) return;
items.forEach((item, index) => {
let zPos = (scrollProgress * 1.5) - (index * Z_SPACING) + START_Z;
let opacity = 1;
if (zPos > 500) {
opacity = 1 - ((zPos - 500) / 400);
}
if (zPos < -2000) {
opacity = 1 - (Math.abs(zPos + 2000) / 2000);
}
if (opacity < 0) opacity = 0;
(item as HTMLElement).style.transform = `translateZ(${zPos}px)`;
(item as HTMLElement).style.opacity = opacity.toString();
});
requestAnimationFrame(updateScroll);
}
window.addEventListener('scroll', () => {
requestAnimationFrame(updateScroll);
});
updateScroll();
</script>
+75
View File
@@ -0,0 +1,75 @@
---
// src/components/WaveBackground.astro
const { opacity = "opacity-30", theme = "cyan" } = Astro.props;
// Definiera färger baserat på tema
// Cyan = Originalet (Startsidan bas)
// Purple = Contact-sidan (Matchar boxen)
const mainColor = theme === 'purple' ? '#c026d3' : '#4fd1c5'; // Purple-600 vs Teal-400
const secondaryColor = theme === 'purple' ? '#db2777' : '#22d3ee'; // Pink-600 vs Cyan-400
---
<div class={`fixed inset-0 w-full h-full pointer-events-none -z-10 bg-neutral-900 ${opacity}`}>
<svg class="w-full h-full" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="gradMain" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style={`stop-color:${mainColor};stop-opacity:0`} />
<stop offset="50%" style={`stop-color:${mainColor};stop-opacity:0.5`} />
<stop offset="100%" style={`stop-color:${mainColor};stop-opacity:0`} />
</linearGradient>
<linearGradient id="gradSec" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style={`stop-color:${secondaryColor};stop-opacity:0`} />
<stop offset="30%" style={`stop-color:${secondaryColor};stop-opacity:0.3`} />
<stop offset="100%" style={`stop-color:${secondaryColor};stop-opacity:0`} />
</linearGradient>
</defs>
<g id="wave-group">
<path id="wave1" d="M-1000 300 Q 250 100 800 300 T 2600 300" fill="none" stroke="url(#gradMain)" stroke-width="1" />
<path id="wave2" d="M-1000 500 Q 250 700 800 500 T 2600 500" fill="none" stroke="url(#gradMain)" stroke-width="1.5" />
<path id="wave3" d="M-1000 700 Q 250 500 800 700 T 2600 700" fill="none" stroke="url(#gradMain)" stroke-width="0.5" />
<path id="thread1" d="M-1000 200 Q 400 400 900 200 T 2600 200" fill="none" stroke="url(#gradSec)" stroke-width="0.5" class="opacity-50" />
<path id="thread2" d="M-1000 800 Q 600 600 1100 800 T 2600 800" fill="none" stroke="url(#gradSec)" stroke-width="0.8" class="opacity-60" />
</g>
</svg>
</div>
<script>
const wave1 = document.getElementById('wave1');
const wave2 = document.getElementById('wave2');
const wave3 = document.getElementById('wave3');
const thread1 = document.getElementById('thread1');
const thread2 = document.getElementById('thread2');
// Olika hastigheter för parallax-effekt
const speeds = {
w1: 0.5,
w2: -0.8,
w3: 1.2,
t1: 1.8, // Snabbare "random" tråd
t2: -1.5 // Snabbare "random" tråd åt andra hållet
};
function onScroll() {
const scrollY = window.scrollY;
if(wave1) wave1.style.transform = `translateX(${scrollY * speeds.w1}px)`;
if(wave2) wave2.style.transform = `translateX(${scrollY * speeds.w2}px)`;
if(wave3) wave3.style.transform = `translateX(${scrollY * speeds.w3}px)`;
// Random trådarna rör sig mer
if(thread1) thread1.style.transform = `translateX(${scrollY * speeds.t1}px)`;
if(thread2) thread2.style.transform = `translateX(${scrollY * speeds.t2}px)`;
}
window.addEventListener('scroll', onScroll, { passive: true });
</script>
<style>
path {
transition: transform 0.1s linear;
will-change: transform;
}
</style>
+210
View File
@@ -0,0 +1,210 @@
---
import astroLogo from '../assets/astro.svg';
import background from '../assets/background.svg';
---
<div id="container">
<img id="background" src={background.src} alt="" fetchpriority="high" />
<main>
<section id="hero">
<a href="https://astro.build"
><img src={astroLogo.src} width="115" height="48" alt="Astro Homepage" /></a
>
<h1>
To get started, open the <code><pre>src/pages</pre></code> directory in your project.
</h1>
<section id="links">
<a class="button" href="https://docs.astro.build">Read our docs</a>
<a href="https://astro.build/chat"
>Join our Discord <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36"
><path
fill="currentColor"
d="M107.7 8.07A105.15 105.15 0 0 0 81.47 0a72.06 72.06 0 0 0-3.36 6.83 97.68 97.68 0 0 0-29.11 0A72.37 72.37 0 0 0 45.64 0a105.89 105.89 0 0 0-26.25 8.09C2.79 32.65-1.71 56.6.54 80.21a105.73 105.73 0 0 0 32.17 16.15 77.7 77.7 0 0 0 6.89-11.11 68.42 68.42 0 0 1-10.85-5.18c.91-.66 1.8-1.34 2.66-2a75.57 75.57 0 0 0 64.32 0c.87.71 1.76 1.39 2.66 2a68.68 68.68 0 0 1-10.87 5.19 77 77 0 0 0 6.89 11.1 105.25 105.25 0 0 0 32.19-16.14c2.64-27.38-4.51-51.11-18.9-72.15ZM42.45 65.69C36.18 65.69 31 60 31 53s5-12.74 11.43-12.74S54 46 53.89 53s-5.05 12.69-11.44 12.69Zm42.24 0C78.41 65.69 73.25 60 73.25 53s5-12.74 11.44-12.74S96.23 46 96.12 53s-5.04 12.69-11.43 12.69Z"
></path></svg
>
</a>
</section>
</section>
</main>
<a href="https://astro.build/blog/astro-5/" id="news" class="box">
<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"
><path
d="M24.667 12c1.333 1.414 2 3.192 2 5.334 0 4.62-4.934 5.7-7.334 12C18.444 28.567 18 27.456 18 26c0-4.642 6.667-7.053 6.667-14Zm-5.334-5.333c1.6 1.65 2.4 3.43 2.4 5.333 0 6.602-8.06 7.59-6.4 17.334C13.111 27.787 12 25.564 12 22.666c0-4.434 7.333-8 7.333-16Zm-6-5.333C15.111 3.555 16 5.556 16 7.333c0 8.333-11.333 10.962-5.333 22-3.488-.774-6-4-6-8 0-8.667 8.666-10 8.666-20Z"
fill="#111827"></path></svg
>
<h2>What's New in Astro 5.0?</h2>
<p>
From content layers to server islands, click to learn more about the new features and
improvements in Astro 5.0
</p>
</a>
</div>
<style>
#background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
filter: blur(100px);
}
#container {
font-family: Inter, Roboto, 'Helvetica Neue', 'Arial Nova', 'Nimbus Sans', Arial, sans-serif;
height: 100%;
}
main {
height: 100%;
display: flex;
justify-content: center;
}
#hero {
display: flex;
align-items: start;
flex-direction: column;
justify-content: center;
padding: 16px;
}
h1 {
font-size: 22px;
margin-top: 0.25em;
}
#links {
display: flex;
gap: 16px;
}
#links a {
display: flex;
align-items: center;
padding: 10px 12px;
color: #111827;
text-decoration: none;
transition: color 0.2s;
}
#links a:hover {
color: rgb(78, 80, 86);
}
#links a svg {
height: 1em;
margin-left: 8px;
}
#links a.button {
color: white;
background: linear-gradient(83.21deg, #3245ff 0%, #bc52ee 100%);
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.12),
inset 0 -2px 0 rgba(0, 0, 0, 0.24);
border-radius: 10px;
}
#links a.button:hover {
color: rgb(230, 230, 230);
box-shadow: none;
}
pre {
font-family:
ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono',
monospace;
font-weight: normal;
background: linear-gradient(14deg, #d83333 0%, #f041ff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin: 0;
}
h2 {
margin: 0 0 1em;
font-weight: normal;
color: #111827;
font-size: 20px;
}
p {
color: #4b5563;
font-size: 16px;
line-height: 24px;
letter-spacing: -0.006em;
margin: 0;
}
code {
display: inline-block;
background:
linear-gradient(66.77deg, #f3cddd 0%, #f5cee7 100%) padding-box,
linear-gradient(155deg, #d83333 0%, #f041ff 18%, #f5cee7 45%) border-box;
border-radius: 8px;
border: 1px solid transparent;
padding: 6px 8px;
}
.box {
padding: 16px;
background: rgba(255, 255, 255, 1);
border-radius: 16px;
border: 1px solid white;
}
#news {
position: absolute;
bottom: 16px;
right: 16px;
max-width: 300px;
text-decoration: none;
transition: background 0.2s;
backdrop-filter: blur(50px);
}
#news:hover {
background: rgba(255, 255, 255, 0.55);
}
@media screen and (max-height: 368px) {
#news {
display: none;
}
}
@media screen and (max-width: 768px) {
#container {
display: flex;
flex-direction: column;
}
#hero {
display: block;
padding-top: 10%;
}
#links {
flex-wrap: wrap;
}
#links a.button {
padding: 14px 18px;
}
#news {
right: 16px;
left: 16px;
bottom: 2.5rem;
max-width: 100%;
}
h1 {
line-height: 1.5;
}
}
</style>
+22
View File
@@ -0,0 +1,22 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="generator" content={Astro.generator} />
<title>Astro Basics</title>
</head>
<body>
<slot />
</body>
</html>
<style>
html,
body {
margin: 0;
width: 100%;
height: 100%;
}
</style>
+37
View File
@@ -0,0 +1,37 @@
---
import "../styles/global.css";
const { title } = Astro.props;
---
<html lang="sv" class="scroll-smooth"> <head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>{title}</title>
</head>
<body class="bg-neutral-900 text-white font-sans antialiased min-h-screen flex flex-col">
<nav class="fixed top-0 w-full z-50 border-b border-white/10 py-6 bg-neutral-900/80 backdrop-blur-md transition-all duration-300">
<div class="container mx-auto px-4 flex justify-between items-center">
<a href="/" class="text-2xl font-bold tracking-wider uppercase hover:text-gray-300 transition relative z-50">
Images by Olofsson
</a>
<ul class="flex space-x-6 text-sm font-medium text-gray-400">
<li><a href="/portfolio" class="hover:text-white transition">Portfolio</a></li>
<li><a href="/about" class="hover:text-white transition">About</a></li>
<li><a href="/contact" class="hover:text-white transition">Contact</a></li>
</ul>
</div>
</nav>
<main class="flex-grow pt-24">
<slot />
</main>
<footer class="border-t border-neutral-800 py-8 text-center text-gray-500 text-sm bg-neutral-900 relative z-40">
<p>&copy; {new Date().getFullYear()} Images by Olofsson. All rights reserved.</p>
</footer>
</body>
</html>
+127
View File
@@ -0,0 +1,127 @@
---
// 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';
---
<MainLayout title="About | Images by Olofsson">
<WaveBackground opacity="opacity-80" />
<div class="container mx-auto px-4 py-12 md:py-20 relative z-10">
<div 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="Linus Olofsson Photographer"
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">
Capturing moments, <br/>
<span class="text-transparent bg-clip-text bg-gradient-to-r from-white to-gray-500">preserving memories.</span>
</h1>
<div class="space-y-4 text-gray-300 text-lg leading-relaxed">
<p>
Hi — I’m Linus Olofsson.
Cloud specialist by day, passionate photographer by night.
</p>
<p>
Based in Sweden, my journey began with a simple curiosity for how light shapes the way we perceive the world. Over time, that curiosity evolved into an obsession with capturing the subtle details most people overlook — textures, structures, contrasts and moments that flicker by unnoticed.
</p>
<p>
I find beauty in the small things: a stone, a tree, an empty street, a quiet forest path, or the expression of a stranger passing by.
</p>
<p>
Whether it’s the intricate patterns of a macro shot, the vast silence of nature, or the energy of a city, my goal is
always the same — to create images that convey emotion and tell a story without words.
</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">
Get in touch
</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">
View the world through my eyes
</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">What's in my bag?</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<div 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">Camera Body</h3>
<p class="text-gray-400">Canon EOS 1Ds Mk II</p>
<p class="text-gray-500 text-sm mt-2">A trusty companion that has been with me on countless adventures.</p>
</div>
<div 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">Lenses</h3>
<ul class="text-gray-400 text-sm space-y-1">
<li>Canon EF 70-200mm f/4L USM</li>
<li>Canon EF 20mm f/2.8 USM</li>
<li>and a couple of extra batteries</li>
</ul>
</div>
<div 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">Editing</h3>
<p class="text-gray-400">Adobe Lightroom Classic</p>
<p class="text-gray-500 text-sm mt-2">Where the raw captures come to life. I focus on natural tones and contrast.</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>
+128
View File
@@ -0,0 +1,128 @@
---
// src/pages/contact.astro
import MainLayout from '../layouts/MainLayout.astro';
import WaveBackground from '../components/WaveBackground.astro';
---
<MainLayout title="Contact | Images by Olofsson">
<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="container mx-auto px-4 py-12 md:py-20">
<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">Get in touch</h1>
<p class="text-gray-400 text-lg leading-relaxed">
Do you have questions about any of my photos, collaborations, or just want to say hi?
Connect with me on social media or send me a direct email.
</p>
</div>
<div>
<h2 class="text-white font-semibold text-xl mb-4">You can also find me on...</h2>
<div class="grid grid-cols-3 gap-4">
<a href="https://www.instagram.com/imagesbyolofsson/" 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="https://www.flickr.com/photos/imagesbyolofsson/" 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="https://lightroom.adobe.com/u/linusolofsson1" 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="https://github.com/LinusOlofsson-maker" 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="https://www.linkedin.com/in/linus-olofsson-/" 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:info@imagesbyolofsson.se" class="social-card group" aria-label="Email">
<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 class="relative bg-neutral-900 ring-1 ring-white/10 rounded-xl p-8 md:p-12 text-center space-y-10">
<div>
<h3 class="text-2xl font-bold text-white mb-2">Waiting to hear from you</h3>
<p class="text-gray-400">Feel free to reach out for inquiries or just a chat.</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-500 uppercase tracking-widest mb-2">Email me at</p>
<button id="email-btn" class="text-xl md:text-3xl font-bold text-white hover:text-purple-400 transition-colors break-all" data-email="info@imagesbyolofsson.se">
info@imagesbyolofsson.se
</button>
<p id="copy-feedback" class="h-4 text-xs text-green-400 mt-2 opacity-0 transition-opacity">Copied to clipboard!</p>
</div>
</div>
<div class="pt-2">
<p class="text-sm text-gray-500 uppercase tracking-widest mb-4">Or DM me on Instagram</p>
<a href="https://www.instagram.com/imagesbyolofsson/" target="_blank" class="inline-flex items-center gap-2 text-xl font-bold text-white hover:text-pink-500 transition-colors">
@imagesbyolofsson
</a>
</div>
</div>
</div>
</div>
</div>
</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>
<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>
+258
View File
@@ -0,0 +1,258 @@
---
// src/pages/index.astro
import MainLayout from '../layouts/MainLayout.astro';
import ThreeDScroll from '../components/ThreeDScroll.astro';
import WaveBackground from '../components/WaveBackground.astro';
---
<MainLayout title="Home | Images by Olofsson">
<div id="wave-wrapper" class="fixed inset-0 z-0 pointer-events-none transition-opacity duration-1000 ease-in-out" style="opacity: 0.4;">
<WaveBackground opacity="opacity-100" theme="cyan" />
</div>
<div id="hero-text" class="fixed top-0 left-0 w-full h-screen flex flex-col items-center justify-center z-10 pointer-events-none opacity-0 transition-all duration-700 ease-in-out">
<div class="text-center animated-text-container relative">
<div id="text-wrapper" class="inline-block relative p-6 transition-all duration-500">
<h1 class="text-5xl md:text-8xl font-black text-white tracking-tighter leading-none drop-shadow-[0_0_10px_rgba(34,211,238,0.4)] relative z-10">
IMAGES BY <br/> OLOFSSON
</h1>
<svg class="absolute inset-0 w-full h-full z-0 overflow-visible pointer-events-none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="none">
<defs>
<filter id="fog-filter" x="-100%" y="-100%" width="300%" height="300%">
<feGaussianBlur stdDeviation="15" result="blurredFog"/>
<feComponentTransfer in="blurredFog">
<feFuncA type="table" tableValues="0 0.1 0.3 1"/>
</feComponentTransfer>
</filter>
</defs>
<path id="outline-path" d="M 0,2 L 100,2 L 100,50 L 0,50 L 0,98 L 100,98" fill="none" stroke-linecap="round" vector-effect="non-scaling-stroke" />
<use href="#outline-path" class="smoky-path" stroke-width="40" style="filter: url(#fog-filter); opacity: 0.6;" />
</svg>
</div>
<p id="scroll-arrow" class="text-white/70 mt-6 text-lg font-light tracking-widest uppercase transition-opacity duration-300">↓</p>
</div>
</div>
<div id="images-container" class="relative z-20 transition-opacity duration-700 ease-in-out">
<ThreeDScroll />
</div>
<div id="footer-section" class="h-[50vh] flex items-center justify-center bg-neutral-900 z-30 relative border-t border-white/10">
<div class="flex flex-col md:flex-row gap-12 md:gap-24 items-center text-center">
<div class="flex flex-col items-center">
<h2 class="text-3xl md:text-4xl text-white font-bold mb-6">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">
My Portfolio
</a>
</div>
<div class="flex flex-row md:flex-col gap-6 items-center justify-center">
<a href="https://www.instagram.com/imagesbyolofsson/" target="_blank" rel="noopener noreferrer" class="circle-btn group" aria-label="Instagram">
<svg class="outline-ring" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="48" />
</svg>
<div class="icon-wrapper group-hover:text-pink-500">
<svg viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6">
<path d="M7.8 2h8.4C19.4 2 22 4.6 22 7.8v8.4a5.8 5.8 0 0 1-5.8 5.8H7.8C4.6 22 2 19.4 2 16.2V7.8A5.8 5.8 0 0 1 7.8 2m-.2 2A3.6 3.6 0 0 0 4 7.6v8.8C4 18.39 5.61 20 7.6 20h8.8a3.6 3.6 0 0 0 3.6-3.6V7.6C20 5.61 18.39 4 16.4 4H7.6m9.65 1.5a1.25 1.25 0 0 1 1.25 1.25A1.25 1.25 0 0 1 17.25 8 1.25 1.25 0 0 1 16 6.75a1.25 1.25 0 0 1 1.25-1.25M12 7a5 5 0 0 1 5 5 5 5 0 0 1-5 5 5 5 0 0 1-5-5 5 5 0 0 1 5-5m0 2a3 3 0 0 0-3 3 3 3 0 0 0 3 3 3 3 0 0 0 3-3 3 3 0 0 0-3-3z"/>
</svg>
</div>
</a>
<a href="https://lightroom.adobe.com/u/linusolofsson1" target="_blank" rel="noopener noreferrer" class="circle-btn group" aria-label="Lightroom">
<svg class="outline-ring" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="48" />
</svg>
<div class="icon-wrapper group-hover:text-cyan-400">
<svg viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6"><path d="M4 4v16h11v-4H8V4H4zm13 4c-2.21 0-4 1.79-4 4v8h4v-7c0-1.1.9-2 2-2v-3h-2z"/></svg>
</div>
</a>
</div>
<div class="flex flex-col items-center">
<h2 class="text-3xl md:text-4xl text-white font-bold mb-6">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">
Contact me
</a>
</div>
</div>
</div>
</MainLayout>
<style>
@reference "../styles/global.css";
/* --- RÖK-ANIMATIONEN --- */
.smoky-path {
stroke-dasharray: var(--length);
stroke-dashoffset: var(--length);
animation: drawPath 6s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}
@keyframes drawPath {
0% { stroke-dashoffset: var(--length); opacity: 0; }
10% { opacity: 1; }
85% { stroke-dashoffset: 0; opacity: 1; }
100% { stroke-dashoffset: 0; opacity: 0; }
}
.smoky-path {
animation: drawPath 6s cubic-bezier(0.4, 0, 0.2, 1) infinite, smokeShift 10s ease-in-out infinite alternate;
}
@keyframes smokeShift {
0% { stroke: #374151; }
33% { stroke: #4b5563; }
66% { stroke: #334155; }
100% { stroke: #1f2937; }
}
/* --- BRACKETS (Hörnen) --- */
#text-wrapper::before,
#text-wrapper::after {
content: "";
position: absolute;
width: 63%;
height: 40%;
border-color: #878681;
border-style: solid;
border-width: 0;
opacity: 0;
transition: opacity 0.8s ease, transform 0.8s ease;
}
/* Övre vänstra */
#text-wrapper::before {
top: 0;
left: 0;
border-top-width: 3px;
border-left-width: 3px;
transform: translate(-10px, -10px);
}
/* Nedre högra */
#text-wrapper::after {
bottom: 0;
right: 0;
border-bottom-width: 3px;
border-right-width: 3px;
transform: translate(10px, 10px);
}
/* --- KLASSEN SOM JS LÄGGER PÅ --- */
#text-wrapper.show-brackets::before,
#text-wrapper.show-brackets::after {
opacity: 1;
transform: translate(0, 0);
}
/* --- FOOTER ICONER --- */
.circle-btn {
position: relative;
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: rgba(255, 255, 255, 0.03);
transition: all 0.3s ease;
}
.icon-wrapper { color: rgba(255, 255, 255, 0.6); transition: color 0.3s ease, transform 0.3s ease; z-index: 10; }
.circle-btn:hover .icon-wrapper { transform: scale(1.1); }
.outline-ring { position: absolute; top: 0; left: 0; width: 100%; height: 100%; transform: rotate(-90deg); pointer-events: none; }
.outline-ring circle { fill: none; stroke: white; stroke-width: 2; stroke-dasharray: 302; stroke-dashoffset: 302; transition: stroke-dashoffset 0.6s cubic-bezier(0.25, 1, 0.5, 1); opacity: 0.8; }
.circle-btn:hover .outline-ring circle { stroke-dashoffset: 0; }
</style>
<script>
// SVG Längd
const path = document.getElementById('outline-path') as unknown as SVGPathElement;
if (path) {
const length = path.getTotalLength();
path.closest('svg')?.style.setProperty('--length', length.toString());
}
const heroText = document.getElementById('hero-text');
const textWrapper = document.getElementById('text-wrapper');
const arrow = document.getElementById('scroll-arrow');
const smokyPath = document.querySelector('.smoky-path') as SVGElement;
const waveWrapper = document.getElementById('wave-wrapper'); // Hämta bakgrunden
// LOGIK FÖR GRADVIS FADE BASERAT PÅ SCROLL
window.addEventListener('scroll', () => {
const scrollY = window.scrollY;
const windowHeight = window.innerHeight;
if (arrow) {
arrow.style.opacity = scrollY > 50 ? '0' : '1';
}
if (heroText && !footerIsVisible) {
const fadeProgress = Math.min(scrollY / (windowHeight * 3), 1);
heroText.style.opacity = fadeProgress.toString();
}
if (scrollY > 2500) {
if (smokyPath) smokyPath.style.animationDuration = '4s';
} else {
if (smokyPath) smokyPath.style.animationDuration = '6s';
}
});
// FOOTER INTERAKTION
const footer = document.getElementById('footer-section');
const imagesContainer = document.getElementById('images-container');
let footerIsVisible = false;
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
footerIsVisible = entry.isIntersecting;
if (entry.isIntersecting) {
// -- FOOTER SYNS --
if (imagesContainer) imagesContainer.style.opacity = '0';
if (heroText) {
heroText.style.opacity = '1';
heroText.style.transform = 'translateY(-25vh)';
}
// Tänd Brackets
if (textWrapper) textWrapper.classList.add('show-brackets');
// HÖJ OPACITY TILL 90% (Hårdkodat via style)
if (waveWrapper) {
waveWrapper.style.opacity = '0.9';
}
} else {
// -- FOOTER SYNS INTE --
if (imagesContainer) imagesContainer.style.opacity = '1';
if (heroText) {
heroText.style.transform = 'translateY(0)';
}
// Släck Brackets
if (textWrapper) textWrapper.classList.remove('show-brackets');
// ÅTERSTÄLL OPACITY TILL 40% (Hårdkodat via style)
if (waveWrapper) {
waveWrapper.style.opacity = '0.4';
}
}
});
}, { threshold: 0.1 });
if (footer) observer.observe(footer);
</script>
+322
View File
@@ -0,0 +1,322 @@
---
// src/pages/portfolio.astro
import MainLayout from '../layouts/MainLayout.astro';
import { Image } from 'astro:assets';
import exifr from 'exifr';
import fs from 'node:fs';
import path from 'node:path';
import WaveBackground from '../components/WaveBackground.astro';
// --- SERVER-SIDE LOGIC ---
// 1. Hämta alla bilder
const imageFiles = import.meta.glob('../assets/images/portfolio/*.{jpeg,jpg,png,webp,JPG,JPEG,PNG}', { eager: true });
function formatShutterSpeed(exposureTime: number) {
if (!exposureTime) return null;
if (exposureTime >= 1) return exposureTime.toString();
const fraction = Math.round(1 / exposureTime);
return `1/${fraction}`;
}
const galleryItems = await Promise.all(Object.entries(imageFiles).map(async ([relativePath, file]: [string, any]) => {
let exifData: any = {};
let dateTaken = null;
const absolutePath = path.resolve('./src/pages', relativePath);
try {
const fileBuffer = fs.readFileSync(absolutePath);
exifData = await exifr.parse(fileBuffer, [
'ISO', 'Model', 'FNumber', 'ExposureTime', 'FocalLength', 'DateTimeOriginal', 'CreateDate'
]);
dateTaken = exifData?.DateTimeOriginal || exifData?.CreateDate;
if (!dateTaken) dateTaken = new Date();
} catch (e) {
console.error(`Kunde inte läsa EXIF för ${relativePath}`);
dateTaken = new Date();
}
const dateObj = new Date(dateTaken);
const year = dateObj.getFullYear().toString();
const month = dateObj.toLocaleString('sv-SE', { month: 'long' }).toLowerCase();
const monthIndex = dateObj.getMonth();
const fullDate = dateObj.toLocaleDateString('sv-SE', {
day: 'numeric', month: 'long', year: 'numeric'
});
const shutterSpeed = formatShutterSpeed(exifData?.ExposureTime);
const focalLength = exifData?.FocalLength ? `${Math.round(exifData.FocalLength)}mm` : null;
const fStop = exifData?.FNumber ? `f/${exifData.FNumber}` : null;
const iso = exifData?.ISO ? `ISO ${exifData.ISO}` : null;
return {
...file, year, month, monthIndex, fullDate, model: exifData?.Model,
fStop, shutterSpeed, iso, focalLength
};
}));
const uniqueYears = [...new Set(galleryItems.map((item: any) => item.year))].sort().reverse();
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);
---
<MainLayout title="Portfolio | Images by Olofsson">
<div>
<WaveBackground opacity="opacity-55" />
</div>
<div class="container mx-auto px-4 py-12">
<div class="text-center mb-12">
<h1 class="text-4xl md:text-6xl font-bold mb-4">Portfolio</h1>
<p class="text-gray-400">A curated selection of my work across different styles.</p>
</div>
<div class="flex flex-col items-center gap-6 mb-12">
<div class="flex flex-wrap justify-center gap-2">
<button class="filter-btn active" data-filter="all" data-type="year">Alla år</button>
{uniqueYears.map(year => (
<button class="filter-btn" data-filter={year} data-type="year">{year}</button>
))}
</div>
<div class="flex flex-wrap justify-center gap-2">
<button class="filter-btn active" data-filter="all" data-type="month">Alla månader</button>
{uniqueMonths.map(month => (
<button class="filter-btn capitalize" data-filter={month} data-type="month">{month}</button>
))}
</div>
</div>
<div class="columns-1 md:columns-2 lg:columns-3 gap-6 space-y-6" id="gallery-grid">
{galleryItems.map((item: any) => (
<div
class="gallery-item 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.default.src}
data-model={item.model}
data-iso={item.iso}
data-fstop={item.fStop}
data-shutter={item.shutterSpeed}
data-focal={item.focalLength}
data-date={item.fullDate}
>
<Image
src={item.default}
alt={`Foto taget med ${item.model}`}
width={800}
class="w-full h-auto object-cover transform transition duration-700 group-hover:scale-105"
/>
<div class="absolute inset-0 bg-gradient-to-t from-black/90 via-black/40 to-transparent opacity-0 group-hover:opacity-100 transition duration-300 flex flex-col justify-end p-6 pointer-events-none">
<p class="text-white font-bold text-lg">{item.fullDate}</p>
<p class="text-xs text-gray-400 uppercase tracking-widest mt-1">Visa bild</p>
</div>
</div>
))}
</div>
<div id="no-results" class="hidden text-center text-gray-500 mt-20">
<p class="text-xl">Inga bilder hittades för denna tidpunkt.</p>
<button id="reset-btn" class="mt-4 text-white underline hover:text-gray-300">Visa alla bilder</button>
</div>
</div>
<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 id="lb-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="Full size" class="max-h-full max-w-full w-full h-full object-contain p-2 md:p-4" />
<button id="lb-prev" 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 id="lb-next" 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>
<div class="bg-neutral-800 p-4 md:p-6 shrink-0 border-t border-white/5 flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
<div class="text-left">
<h3 id="lb-date" class="text-white font-bold text-lg md:text-xl capitalize"></h3>
<p id="lb-model" class="text-gray-400 text-sm font-medium tracking-wide mt-1"></p>
</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-col md:flex-row md:items-center gap-1"><span class="text-gray-500 uppercase text-[10px] font-bold">Focal</span><span id="lb-focal">--</span></div>
<div class="flex flex-col md:flex-row md:items-center gap-1"><span class="text-gray-500 uppercase text-[10px] font-bold">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-500 uppercase text-[10px] font-bold">Shutter</span><span id="lb-shutter">--</span></div>
<div class="flex flex-col md:flex-row md:items-center gap-1"><span class="text-gray-500 uppercase text-[10px] font-bold">ISO</span><span id="lb-iso">--</span></div>
</div>
</div>
</div>
</div>
</MainLayout>
<style>
@reference "../styles/global.css";
.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;
}
.filter-btn.active {
@apply bg-white text-black border-white font-medium shadow-[0_0_15px_rgba(255,255,255,0.3)];
}
</style>
<script>
// --- FILTER LOGIK ---
let currentYear = 'all';
let currentMonth = 'all';
const items = Array.from(document.querySelectorAll('.gallery-item')) as HTMLElement[];
const buttons = document.querySelectorAll('.filter-btn');
const noResults = document.getElementById('no-results');
const resetBtn = document.getElementById('reset-btn');
function filterImages() {
let visibleCount = 0;
items.forEach((el) => {
const itemYear = el.dataset.year;
const itemMonth = el.dataset.month;
const matchYear = currentYear === 'all' || itemYear === currentYear;
const matchMonth = currentMonth === 'all' || itemMonth === currentMonth;
if (matchYear && matchMonth) {
el.style.display = 'block';
el.classList.add('visible-item');
if(el.style.opacity === '0') {
el.animate([{ opacity: 0, transform: 'scale(0.95)' }, { opacity: 1, transform: 'scale(1)' }], { duration: 300, easing: 'ease-out' });
}
visibleCount++;
} else {
el.style.display = 'none';
el.classList.remove('visible-item');
}
});
if (noResults) noResults.style.display = visibleCount === 0 ? 'block' : 'none';
}
// --- LIGHTBOX LOGIK ---
const lightbox = document.getElementById('lightbox');
const lbImg = document.getElementById('lb-img') as HTMLImageElement;
const lbDate = document.getElementById('lb-date');
const lbModel = document.getElementById('lb-model');
const lbFocal = document.getElementById('lb-focal');
const lbFstop = document.getElementById('lb-fstop');
const lbShutter = document.getElementById('lb-shutter');
const lbIso = document.getElementById('lb-iso');
const closeBtn = document.getElementById('lb-close');
const nextBtn = document.getElementById('lb-next');
const prevBtn = document.getElementById('lb-prev');
let currentImageIndex = 0;
let visibleItems: HTMLElement[] = [];
function updateVisibleItems() {
visibleItems = items.filter(item => item.style.display !== 'none');
}
function openLightbox(index: number) {
updateVisibleItems();
if (visibleItems.length === 0) return;
currentImageIndex = index;
updateLightboxContent();
lightbox?.classList.remove('hidden');
setTimeout(() => { lightbox?.classList.remove('opacity-0'); }, 10);
document.body.style.overflow = 'hidden';
}
function closeLightbox() {
lightbox?.classList.add('opacity-0');
setTimeout(() => {
lightbox?.classList.add('hidden');
document.body.style.overflow = '';
}, 300);
}
function updateLightboxContent() {
const item = visibleItems[currentImageIndex];
if (!item) return;
lbImg.src = item.dataset.src || '';
if(lbDate) lbDate.innerText = item.dataset.date || '';
if(lbModel) lbModel.innerText = item.dataset.model || '';
if(lbFocal) lbFocal.innerText = item.dataset.focal || '--';
if(lbFstop) lbFstop.innerText = item.dataset.fstop || '--';
if(lbShutter) lbShutter.innerText = item.dataset.shutter ? item.dataset.shutter + 's' : '--';
if(lbIso) lbIso.innerText = item.dataset.iso || '--';
}
function showNext() {
currentImageIndex = (currentImageIndex + 1) % visibleItems.length;
updateLightboxContent();
}
function showPrev() {
currentImageIndex = (currentImageIndex - 1 + visibleItems.length) % visibleItems.length;
updateLightboxContent();
}
// Event Listeners
items.forEach((item) => {
item.classList.add('visible-item');
item.addEventListener('click', () => {
updateVisibleItems();
const index = visibleItems.indexOf(item);
if (index !== -1) openLightbox(index);
});
});
closeBtn?.addEventListener('click', closeLightbox);
nextBtn?.addEventListener('click', (e) => { e.stopPropagation(); showNext(); });
prevBtn?.addEventListener('click', (e) => { e.stopPropagation(); showPrev(); });
lightbox?.addEventListener('click', (e) => {
if (e.target === lightbox) closeLightbox();
});
document.addEventListener('keydown', (e) => {
if (lightbox?.classList.contains('hidden')) return;
if (e.key === 'Escape') closeLightbox();
if (e.key === 'ArrowRight') showNext();
if (e.key === 'ArrowLeft') showPrev();
});
// Filter Listeners
buttons.forEach(btn => {
btn.addEventListener('click', (e) => {
const target = e.target as HTMLElement;
const type = target.dataset.type;
const value = target.dataset.filter;
if (!type || !value) return;
document.querySelectorAll(`.filter-btn[data-type="${type}"]`).forEach(b => b.classList.remove('active'));
target.classList.add('active');
if (type === 'year') currentYear = value;
if (type === 'month') currentMonth = value;
filterImages();
});
});
resetBtn?.addEventListener('click', () => {
currentYear = 'all';
currentMonth = 'all';
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
document.querySelector('.filter-btn[data-filter="all"][data-type="year"]')?.classList.add('active');
document.querySelector('.filter-btn[data-filter="all"][data-type="month"]')?.classList.add('active');
filterImages();
});
</script>
+1
View File
@@ -0,0 +1 @@
@import "tailwindcss";