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
@@ -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?
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
# Images by Olofsson
|
||||||
|
|
||||||
|
En modern, responsiv fotografportfolio webbplats byggd med Astro, Tailwind CSS och avancerade animationer.
|
||||||
|
|
||||||
|
[](https://imagesbyolofsson.se)
|
||||||
|
[](https://astro.build)
|
||||||
|
[](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
|
||||||
|

|
||||||
|
*Ett snyggt rutnät av bilder med hover-effekter*
|
||||||
|
|
||||||
|
### 3D Scroll Effect
|
||||||
|

|
||||||
|
*Dynamisk 3D-parallax effekt vid scrollning*
|
||||||
|
|
||||||
|
### Hero Section
|
||||||
|

|
||||||
|
*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>
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 700 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 542 KiB |
@@ -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 |
@@ -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 |
@@ -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 |
|
After Width: | Height: | Size: 700 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 542 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 417 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 547 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 768 KiB |
|
After Width: | Height: | Size: 354 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 761 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 716 KiB |
|
After Width: | Height: | Size: 964 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 619 KiB |
|
After Width: | Height: | Size: 424 KiB |
|
After Width: | Height: | Size: 433 KiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 639 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 817 KiB |
|
After Width: | Height: | Size: 1012 KiB |
|
After Width: | Height: | Size: 676 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 865 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 916 KiB |
|
After Width: | Height: | Size: 2.8 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 658 KiB |
|
After Width: | Height: | Size: 560 KiB |
|
After Width: | Height: | Size: 601 KiB |
|
After Width: | Height: | Size: 949 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 838 KiB |
|
After Width: | Height: | Size: 478 KiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 885 KiB |
|
After Width: | Height: | Size: 765 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 561 KiB |
|
After Width: | Height: | Size: 550 KiB |
|
After Width: | Height: | Size: 1009 KiB |
|
After Width: | Height: | Size: 978 KiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>© {new Date().getFullYear()} Images by Olofsson. All rights reserved.</p>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@import "tailwindcss";
|
||||||