B2C travel & tour web application for travelers visiting Lombok and Bali with custom tour building.
# Install dependencies
npm install
# Run development server
npm run devOpen http://localhost:3000 with your browser to see the result.
- Framework: Next.js (App Router)
- Language: TypeScript
- Styling: Tailwind CSS + DaisyUI
- Auth: Google OAuth (NextAuth.js)
- Payment: Xendit
- Deployment: Vercel
Project ini menggunakan sistem optimasi image custom yang mengkonversi image menjadi folder berisi multiple WebP variants untuk performa optimal di web.
npm install -D sharpGunakan script convert-image untuk mengkonversi image menjadi folder optimized WebP:
# Basic — output ke public/assets/<nama-file>/
npm run convert-image -- ./path/to/image.jpg
# Custom output directory
npm run convert-image -- ./path/to/image.jpg --output public/assets/hero
# Custom folder name
npm run convert-image -- ./path/to/image.jpg --name hero-bannerpublic/assets/<nama-image>/
├── placeholder.webp → 20px wide, quality 20 (blur placeholder)
├── sm.webp → 640px wide, quality 80
├── md.webp → 1024px wide, quality 80
├── lg.webp → 1536px wide, quality 80
├── xl.webp → 1920px wide, quality 80
├── original.webp → Original size, quality 90
└── metadata.json → Info: sizes, aspect ratio, dominant color
Semua image di project ini WAJIB menggunakan OptimizedImage component:
import OptimizedImage from "@/components/ui/optimized-image";
// Basic — lazy loaded by default
<OptimizedImage src="/assets/hero-banner" alt="Hero banner" />
// Hero / above-the-fold image — tanpa lazy loading
<OptimizedImage src="/assets/hero-banner" alt="Hero" priority />
// Dengan ukuran tetap
<OptimizedImage src="/assets/destination" alt="Destination" width={400} height={300} />
// Fill parent container
<div style={{ position: "relative", width: "100%", height: "400px" }}>
<OptimizedImage src="/assets/hero" alt="Hero" fill objectFit="cover" />
</div>| Prop | Type | Default | Description |
|---|---|---|---|
src |
string |
required | Path ke folder image (relative to public/) |
alt |
string |
required | Alt text untuk accessibility |
className |
string |
"" |
Class untuk wrapper div |
imgClassName |
string |
"" |
Class untuk elemen img |
style |
CSSProperties |
— | Inline style untuk wrapper |
loading |
"lazy" | "eager" |
"lazy" |
Loading strategy |
sizes |
string |
responsive default | Sizes attribute untuk responsive |
priority |
boolean |
false |
Disable lazy loading, preload image |
objectFit |
CSSProperties["objectFit"] |
"cover" |
CSS object-fit |
objectPosition |
string |
"center" |
CSS object-position |
fill |
boolean |
false |
Fill parent container |
width |
number |
— | Fixed width (px) |
height |
number |
— | Fixed height (px) |
onLoad |
() => void |
— | Callback setelah image loaded |
onError |
() => void |
— | Callback jika error |
- ✅ Blur placeholder — Tiny blurred image ditampilkan saat loading
- ✅ Dominant color background — Background color dari warna dominan image
- ✅ Lazy loading — Image hanya di-load saat masuk viewport (IntersectionObserver)
- ✅ Responsive srcSet — Browser otomatis pilih ukuran optimal
- ✅ Smooth fade-in — Transisi halus dari placeholder ke full image
- ✅ Metadata caching — metadata.json di-cache, tidak fetch ulang
The easiest way to deploy your Next.js app is to use the Vercel Platform from the creators of Next.js.
Check out our Next.js deployment documentation for more details.