remove product-site application and its related assets from the repository
This commit is contained in:
@@ -1,41 +0,0 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
@@ -1,5 +0,0 @@
|
||||
<!-- BEGIN:nextjs-agent-rules -->
|
||||
# This is NOT the Next.js you know
|
||||
|
||||
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
|
||||
<!-- END:nextjs-agent-rules -->
|
||||
@@ -1 +0,0 @@
|
||||
@AGENTS.md
|
||||
@@ -1,36 +0,0 @@
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
@@ -1,3 +0,0 @@
|
||||
import { nextjsConfig } from "@repo/eslint-config/nextjs.mjs";
|
||||
|
||||
export default nextjsConfig;
|
||||
@@ -1,215 +0,0 @@
|
||||
{
|
||||
"Metadata": {
|
||||
"title": "Pintu Masuk — Manage Visitors, From Tickets to Gates",
|
||||
"description": "The First Integrated Ticketing and Access Platform in Indonesia"
|
||||
},
|
||||
"Nav": {
|
||||
"home": "Home",
|
||||
"solutions": "Solutions",
|
||||
"industries": "Industries",
|
||||
"pricing": "Pricing",
|
||||
"contact": "Contact",
|
||||
"demo": "Schedule Demo"
|
||||
},
|
||||
"Hero": {
|
||||
"title1": "Manage Visitors.",
|
||||
"title2": "From Tickets to Gates.",
|
||||
"desc": "Manage the entire visitor journey in one platform, from ticket sales, access validation, queue management, to operational reporting. Keeps running even when internet connection is not always available.",
|
||||
"subtitle": "The First Integrated Ticketing and Access Platform in Indonesia",
|
||||
"cta": "Learn more",
|
||||
"stats": {
|
||||
"timeAccess": "Time access",
|
||||
"seconds": "seconds",
|
||||
"avgValidation": "Avg validation time",
|
||||
"ticketsSold": "Tickets sold",
|
||||
"todayAllChannels": "Today · all channels",
|
||||
"sync": "Synchronization",
|
||||
"txSynced": "transactions Successfully synced",
|
||||
"visitorOps": "Visitor operations",
|
||||
"activeZones": "3 active queue zones"
|
||||
}
|
||||
},
|
||||
"Why": {
|
||||
"title": "Built for non-stop operations",
|
||||
"desc": "More than just a ticketing system, Pintu Masuk is an access infrastructure that keeps working when signals drop, queues lengthen, and when you need quick data-driven decisions.",
|
||||
"items": {
|
||||
"offline": {
|
||||
"title": "Offline-first",
|
||||
"desc": "Transactions remain recorded locally when internet drops, then sync automatically once the network returns."
|
||||
},
|
||||
"speed": {
|
||||
"title": "Speed Validation",
|
||||
"desc": "QR, RFID, and gate barriers validated in milliseconds, even during peak queue times."
|
||||
},
|
||||
"flexibility": {
|
||||
"title": "Flexibility Model",
|
||||
"desc": "Cloud, on-premise, perpetual license, or rental can be tailored to your duration and infrastructure."
|
||||
}
|
||||
}
|
||||
},
|
||||
"Ecosystem": {
|
||||
"title": "Four steps. One ecosystem.",
|
||||
"desc": "Each stage is integrated into one platform, allowing data to flow automatically from ticket purchase to operational reporting without manual processes or repetitive input.",
|
||||
"ticketing": {
|
||||
"title": "Ticketing",
|
||||
"desc": "Sell tickets online via web, apps, or counters. Prices and quotas are always synced in one system."
|
||||
},
|
||||
"access": {
|
||||
"title": "Access Management",
|
||||
"desc": "Validate tickets using QR Code, RFID, or automated gates. Fast, accurate process completed in seconds."
|
||||
},
|
||||
"visitor": {
|
||||
"title": "Visitor Operations",
|
||||
"desc": "Manage queues, zone capacities, and visit schedules in real-time for a more orderly experience."
|
||||
},
|
||||
"report": {
|
||||
"title": "Report",
|
||||
"desc": "Monitor transactions, visitor numbers, occupancy, and operational performance through a single dashboard."
|
||||
},
|
||||
"stats": {
|
||||
"qr": "QR Code",
|
||||
"gate": "Gate Barrier",
|
||||
"rfid": "RFID",
|
||||
"avg": "average",
|
||||
"queue": "124 visitors in queue",
|
||||
"revenue": "Revenue",
|
||||
"visitors": "Visitor",
|
||||
"checkin": "Check-in"
|
||||
}
|
||||
},
|
||||
"Destinations": {
|
||||
"title": "One platform, designed for every type of destination",
|
||||
"desc": "Pintu Masuk adapts to your business processes, because we believe every industry has different visitor flows and operational needs.",
|
||||
"title21": "One platform,",
|
||||
"title22": "Various applications for every need.",
|
||||
"desc2": "Ticket sales, check-ins, to zone occupancies are updated in real-time without needing to wait to know what's happening.",
|
||||
"items": {
|
||||
"themePark": {
|
||||
"title": "Theme Park",
|
||||
"desc": "Manage daily rides and tickets with a more efficient queuing system."
|
||||
},
|
||||
"event": {
|
||||
"title": "Event & Festival",
|
||||
"desc": "Handle thousands of visitors with fast ticket sales and access validation."
|
||||
},
|
||||
"museum": {
|
||||
"title": "Museum",
|
||||
"desc": "Create a more organized visit experience through schedule and capacity management."
|
||||
},
|
||||
"stadium": {
|
||||
"title": "Stadium & Venue",
|
||||
"desc": "Speed up entry processes with accurate ticket validation, even when queues overflow."
|
||||
},
|
||||
"exhibition": {
|
||||
"title": "Exhibition",
|
||||
"desc": "Flexible for multi-day exhibitions with integrated ticket access settings."
|
||||
}
|
||||
},
|
||||
"apps": {
|
||||
"online": {
|
||||
"title": "Online Booking",
|
||||
"desc": "Manage reservations and ticket purchases from various digital channels."
|
||||
},
|
||||
"cashier": {
|
||||
"title": "Buy at Cashier",
|
||||
"desc": "Ticket sales at the cashier to serve visitors directly."
|
||||
},
|
||||
"pos": {
|
||||
"title": "Offline POS (Multi Outlet)",
|
||||
"desc": "Record transactions, redeem bookings, and operate cashiers without relying on internet."
|
||||
},
|
||||
"queue": {
|
||||
"title": "Queue App",
|
||||
"desc": "Helps manage entry queues for a more comfortable visitor experience."
|
||||
},
|
||||
"admin": {
|
||||
"title": "Web Admin",
|
||||
"desc": "Control center to manage master data, transactions, and system synchronization."
|
||||
}
|
||||
}
|
||||
},
|
||||
"Pricing": {
|
||||
"title1": "Start according to needs,",
|
||||
"title2": "grow without limits",
|
||||
"desc": "No need to pay more for unused features. Choose the most suitable package, then upgrade anytime as your operations grow.",
|
||||
"contactUs": "Contact Us",
|
||||
"whatYouGet": "What you get",
|
||||
"rental": {
|
||||
"title": "Rental",
|
||||
"tags": [
|
||||
"Event",
|
||||
"Festival",
|
||||
"Concert"
|
||||
],
|
||||
"desc": "Practical solution for short-duration events. Rent complete devices and systems, ready to use in days.",
|
||||
"f1": "Ticketing & QR check-in",
|
||||
"f2": "Gate & scanner device rental",
|
||||
"f3": "Real-time monitoring dashboard",
|
||||
"f4": "Visitor reports D+1",
|
||||
"f5": "Fast setup & teardown (ready D-1)"
|
||||
},
|
||||
"subscription": {
|
||||
"title": "Subscription",
|
||||
"tags": [
|
||||
"Tourist attraction",
|
||||
"Waterpark"
|
||||
],
|
||||
"desc": "Monthly subscription model for destinations operating daily, without initial infrastructure investment.",
|
||||
"f1": "All Rental features",
|
||||
"f2": "Multi outlet & multi gate",
|
||||
"f3": "Quota & visit schedule management",
|
||||
"f4": "Monthly operational dashboard",
|
||||
"f5": "Automatic feature updates"
|
||||
},
|
||||
"onPremise": {
|
||||
"title": "On-Premise",
|
||||
"tags": [
|
||||
"Enterprise",
|
||||
"Institution"
|
||||
],
|
||||
"desc": "System runs on your own servers and networks, for full control over data and security.",
|
||||
"f1": "All Subscription features",
|
||||
"f2": "Local server installation",
|
||||
"f3": "Internal system integration (SSO, ERP)",
|
||||
"f4": "Advanced access & security control",
|
||||
"f5": "Dedicated deployment support"
|
||||
},
|
||||
"buyOut": {
|
||||
"title": "Buy Out",
|
||||
"tags": [
|
||||
"Company",
|
||||
"Integrator",
|
||||
"Developer"
|
||||
],
|
||||
"desc": "Solution with permanent license for organizations wanting long-term implementation and broader operational control.",
|
||||
"f1": "Permanent License",
|
||||
"f2": "Complete technical documentation",
|
||||
"f3": "Internal engineering team training",
|
||||
"f4": "Free from ongoing license fees",
|
||||
"f5": "Priority support for first year"
|
||||
}
|
||||
},
|
||||
"Contact": {
|
||||
"eyebrow": "Contact us",
|
||||
"title": "Let's discuss your operational needs",
|
||||
"desc": "Our team is ready to help determine the most suitable package for the scale and type of your event or destination.",
|
||||
"email": "Email",
|
||||
"phone": "Phone / WhatsApp",
|
||||
"office": "Office",
|
||||
"form": {
|
||||
"title": "Send a message to our team",
|
||||
"name": "Full name",
|
||||
"needs": "Tell us your needs",
|
||||
"submit": "Send Message",
|
||||
"note": "Our team responds within 1x24 hours on working days.",
|
||||
"placeholderName": "John Doe",
|
||||
"placeholderEmail": "name@company.com",
|
||||
"placeholderNeeds": "Type of event or destination, estimated number of visitors, and the package you are considering."
|
||||
}
|
||||
},
|
||||
"Footer": {
|
||||
"desc": "Manage integrated ticketing and access for theme parks, events, museums, and venues across Indonesia.",
|
||||
"links": "Quick Links",
|
||||
"copyright": "Pintu Masuk. All rights reserved."
|
||||
}
|
||||
}
|
||||
@@ -1,215 +0,0 @@
|
||||
{
|
||||
"Metadata": {
|
||||
"title": "Pintu Masuk — Kelola Pengunjung, Dari Tiket Hingga Gerbang",
|
||||
"description": "Platform Tiket dan Akses Terintegrasi Pertama di Indonesia"
|
||||
},
|
||||
"Nav": {
|
||||
"home": "Beranda",
|
||||
"solutions": "Solusi",
|
||||
"industries": "Industri",
|
||||
"pricing": "Paket",
|
||||
"contact": "Kontak",
|
||||
"demo": "Jadwalkan Demo"
|
||||
},
|
||||
"Hero": {
|
||||
"title1": "Kelola Pengunjung.",
|
||||
"title2": "Dari Tiket Hingga Gerbang.",
|
||||
"desc": "Kelola seluruh perjalanan pengunjung dalam satu platform, mulai dari penjualan tiket, validasi akses, manajemen antrean, hingga pelaporan operasional. Tetap berjalan meski koneksi internet tidak selalu tersedia.",
|
||||
"subtitle": "Platform Tiket dan Akses Terintegrasi Pertama di Indonesia",
|
||||
"cta": "Lihat selengkapnya",
|
||||
"stats": {
|
||||
"timeAccess": "Time access",
|
||||
"seconds": "detik",
|
||||
"avgValidation": "Rerata waktu validasi",
|
||||
"ticketsSold": "Tiket terjual",
|
||||
"todayAllChannels": "Hari ini · semua kanal",
|
||||
"sync": "Sinkronisasi",
|
||||
"txSynced": "transaksi Berhasil disinkronkan",
|
||||
"visitorOps": "Visitor operations",
|
||||
"activeZones": "3 zona antrean aktif"
|
||||
}
|
||||
},
|
||||
"Why": {
|
||||
"title": "Dibangun untuk operasional yang tidak boleh berhenti",
|
||||
"desc": "Bukan hanya sekadar sistem tiket, Pintu Masuk adalah infrastruktur akses yang tetap bekerja saat sinyal hilang, saat antrean memanjang, dan saat kamu butuh keputusan cepat berbasis data.",
|
||||
"items": {
|
||||
"offline": {
|
||||
"title": "Offline-first",
|
||||
"desc": "Transaksi tetap tercatat lokal saat internet putus, lalu sinkron otomatis begitu jaringan kembali."
|
||||
},
|
||||
"speed": {
|
||||
"title": "Speed Validation",
|
||||
"desc": "QR, RFID, dan gate barrier tervalidasi dalam hitungan milidetik, bahkan saat antrean sedang padat."
|
||||
},
|
||||
"flexibility": {
|
||||
"title": "Flexibility Model",
|
||||
"desc": "Cloud, on-premise, perpetual license, atau rental dapat disesuaikan dengan durasi dan infrastruktur kamu."
|
||||
}
|
||||
}
|
||||
},
|
||||
"Ecosystem": {
|
||||
"title": "Empat langkah. Satu ekosistem.",
|
||||
"desc": "Setiap tahap saling terintegrasi dalam satu platform, sehingga data mengalir otomatis dari pembelian tiket hingga pelaporan operasional tanpa proses manual atau input berulang.",
|
||||
"ticketing": {
|
||||
"title": "Ticketing",
|
||||
"desc": "Jual tiket online melalui web, aplikasi, atau loket. Harga dan kuota selalu tersinkron dalam satu sistem."
|
||||
},
|
||||
"access": {
|
||||
"title": "Access Management",
|
||||
"desc": "Validasi tiket menggunakan QR Code, RFID, atau gate otomatis. Proses cepat, akurat, dan selesai dalam hitungan detik."
|
||||
},
|
||||
"visitor": {
|
||||
"title": "Visitor Operations",
|
||||
"desc": "Atur antrean, kapasitas zona, dan jadwal kunjungan secara real-time untuk pengalaman yang lebih tertib."
|
||||
},
|
||||
"report": {
|
||||
"title": "Report",
|
||||
"desc": "Pantau transaksi, jumlah pengunjung, okupansi, dan performa operasional melalui satu dashboard."
|
||||
},
|
||||
"stats": {
|
||||
"qr": "QR Code",
|
||||
"gate": "Gate Barrier",
|
||||
"rfid": "RFID",
|
||||
"avg": "rata-rata",
|
||||
"queue": "124 pengunjung dalam antrean",
|
||||
"revenue": "Revenue",
|
||||
"visitors": "Visitor",
|
||||
"checkin": "Check-in"
|
||||
}
|
||||
},
|
||||
"Destinations": {
|
||||
"title": "Satu platform, dirancang untuk setiap jenis destinasi",
|
||||
"desc": "Pintu Masuk beradaptasi dengan proses bisnis Anda, karena Kami percaya setiap industri memiliki alur pengunjung dan kebutuhan operasional yang berbeda.",
|
||||
"title21": "Satu platform,",
|
||||
"title22": "Berbagai aplikasi untuk setiap kebutuhan.",
|
||||
"desc2": "Penjualan tiket, check-in, hingga okupansi zona diperbarui secara real-time tanpa perlu menunggu untuk tahu apa yang sedang terjadi.",
|
||||
"items": {
|
||||
"themePark": {
|
||||
"title": "Theme Park",
|
||||
"desc": "Kelola wahana dan tiket harian dengan sistem antrean yang lebih efisien."
|
||||
},
|
||||
"event": {
|
||||
"title": "Event & Festival",
|
||||
"desc": "Tangani ribuan pengunjung dengan penjualan tiket dan validasi akses yang cepat."
|
||||
},
|
||||
"museum": {
|
||||
"title": "Museum",
|
||||
"desc": "Ciptakan pengalaman kunjungan yang lebih tertata melalui pengaturan jadwal dan kapasitas."
|
||||
},
|
||||
"stadium": {
|
||||
"title": "Stadium & Venue",
|
||||
"desc": "Percepat proses masuk dengan validasi tiket yang akurat, bahkan saat antrean membludak."
|
||||
},
|
||||
"exhibition": {
|
||||
"title": "Exhibition",
|
||||
"desc": "Fleksibel untuk pameran beberapa hari dengan pengaturan akses tiket yang terintegrasi."
|
||||
}
|
||||
},
|
||||
"apps": {
|
||||
"online": {
|
||||
"title": "Pemesanan Online",
|
||||
"desc": "Kelola reservasi dan pembelian tiket dari berbagai kanal digital."
|
||||
},
|
||||
"cashier": {
|
||||
"title": "Beli di Kasir",
|
||||
"desc": "Penjualan tiket di kasir untuk melayani pengunjung secara langsung."
|
||||
},
|
||||
"pos": {
|
||||
"title": "POS Offline (Multi Outlet)",
|
||||
"desc": "Catat transaksi, redeem booking, dan operasikan kasir tanpa bergantung pada internet."
|
||||
},
|
||||
"queue": {
|
||||
"title": "Queue App",
|
||||
"desc": "Membantu mengatur antrean masuk agar pengalaman pengunjung lebih nyaman."
|
||||
},
|
||||
"admin": {
|
||||
"title": "Web Admin",
|
||||
"desc": "Pusat kontrol untuk mengelola data master, transaksi, dan sinkronisasi sistem."
|
||||
}
|
||||
}
|
||||
},
|
||||
"Pricing": {
|
||||
"title1": "Mulai sesuai kebutuhan,",
|
||||
"title2": "berkembang tanpa batas",
|
||||
"desc": "Tidak perlu membayar lebih untuk fitur yang tidak digunakan. Pilih paket yang paling sesuai, lalu tingkatkan kapan saja seiring berkembangnya operasional Anda.",
|
||||
"contactUs": "Hubungi Kami",
|
||||
"whatYouGet": "Yang kamu dapatkan",
|
||||
"rental": {
|
||||
"title": "Rental",
|
||||
"tags": [
|
||||
"Event",
|
||||
"Festival",
|
||||
"Konser"
|
||||
],
|
||||
"desc": "Solusi praktis untuk acara berdurasi pendek. Sewa perangkat dan sistem lengkap, siap pakai dalam hitungan hari.",
|
||||
"f1": "Ticketing & QR check-in",
|
||||
"f2": "Sewa perangkat gate & scanner",
|
||||
"f3": "Dashboard monitoring real-time",
|
||||
"f4": "Laporan pengunjung H+1",
|
||||
"f5": "Setup & teardown cepat (siap H-1)"
|
||||
},
|
||||
"subscription": {
|
||||
"title": "Subscription",
|
||||
"tags": [
|
||||
"Tempat wisata",
|
||||
"Waterpark"
|
||||
],
|
||||
"desc": "Model langganan bulanan untuk destinasi yang beroperasi setiap hari, tanpa investasi infrastruktur di awal.",
|
||||
"f1": "Semua fitur Rental",
|
||||
"f2": "Multi outlet & multi gate",
|
||||
"f3": "Manajemen kuota & jadwal kunjungan",
|
||||
"f4": "Dashboard operasional bulanan",
|
||||
"f5": "Update fitur otomatis"
|
||||
},
|
||||
"onPremise": {
|
||||
"title": "On-Premise",
|
||||
"tags": [
|
||||
"Enterprise",
|
||||
"Instansi"
|
||||
],
|
||||
"desc": "Sistem berjalan di server dan jaringan milik kamu sendiri, untuk kontrol penuh atas data dan keamanan.",
|
||||
"f1": "Semua fitur Subscription",
|
||||
"f2": "Instalasi di server lokal",
|
||||
"f3": "Integrasi sistem internal (SSO, ERP)",
|
||||
"f4": "Kontrol akses & keamanan lanjutan",
|
||||
"f5": "Dukungan deployment khusus"
|
||||
},
|
||||
"buyOut": {
|
||||
"title": "Buy Out",
|
||||
"tags": [
|
||||
"Perusahaan",
|
||||
"Integrator",
|
||||
"Developer"
|
||||
],
|
||||
"desc": "Solusi dengan lisensi permanen untuk organisasi yang menginginkan implementasi jangka panjang dan kontrol operasional yang lebih luas.",
|
||||
"f1": "Lisensi Permanen",
|
||||
"f2": "Dokumentasi teknis lengkap",
|
||||
"f3": "Pelatihan tim engineering internal",
|
||||
"f4": "Bebas biaya lisensi berkelanjutan",
|
||||
"f5": "Priority support 1 tahun pertama"
|
||||
}
|
||||
},
|
||||
"Contact": {
|
||||
"eyebrow": "Hubungi kami",
|
||||
"title": "Mari diskusikan kebutuhan operasional kamu",
|
||||
"desc": "Tim kami siap membantu menentukan paket yang paling sesuai dengan skala dan jenis acara atau destinasi kamu.",
|
||||
"email": "Email",
|
||||
"phone": "Telepon / WhatsApp",
|
||||
"office": "Kantor",
|
||||
"form": {
|
||||
"title": "Kirim pesan ke tim kami",
|
||||
"name": "Nama lengkap",
|
||||
"needs": "Ceritakan kebutuhan kamu",
|
||||
"submit": "Kirim Pesan",
|
||||
"note": "Tim kami merespons dalam 1x24 jam pada hari kerja.",
|
||||
"placeholderName": "John Doe",
|
||||
"placeholderEmail": "nama@perusahaan.com",
|
||||
"placeholderNeeds": "Jenis acara atau destinasi, estimasi jumlah pengunjung, dan paket yang kamu pertimbangkan."
|
||||
}
|
||||
},
|
||||
"Footer": {
|
||||
"desc": "Kelola tiket dan akses terintegrasi untuk taman hiburan, event, museum, dan venue di seluruh Indonesia.",
|
||||
"links": "Quick Links",
|
||||
"copyright": "Pintu Masuk. Seluruh hak cipta dilindungi."
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import type { NextConfig } from "next";
|
||||
import createNextIntlPlugin from 'next-intl/plugin';
|
||||
|
||||
const withNextIntl = createNextIntlPlugin();
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
};
|
||||
|
||||
export default withNextIntl(nextConfig);
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"name": "product-site",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"copy:brand": "node ../../packages/brand/scripts/copy-brand-assets.js",
|
||||
"dev": "pnpm run copy:brand && next dev",
|
||||
"build": "pnpm run copy:brand && next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint \"src/**/*.ts*\"",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@repo/brand": "workspace:*",
|
||||
"next": "16.2.12",
|
||||
"next-intl": "^4.13.4",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.2.12",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,66 +0,0 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono, Instrument_Sans } from "next/font/google";
|
||||
import { NextIntlClientProvider } from 'next-intl';
|
||||
import { getMessages, getTranslations } from 'next-intl/server';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { routing } from '@/i18n/routing';
|
||||
import "../globals.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const instrumentSans = Instrument_Sans({
|
||||
variable: "--font-instrument-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
||||
const { locale } = await params;
|
||||
const t = await getTranslations({ locale, namespace: 'Metadata' });
|
||||
return {
|
||||
title: t('title'),
|
||||
description: t('description'),
|
||||
icons: {
|
||||
icon: "/brand/favicon.svg"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
params,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
params: Promise<{ locale: string }>;
|
||||
}>) {
|
||||
const { locale } = await params;
|
||||
if (!routing.locales.includes(locale as any)) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const messages = await getMessages();
|
||||
|
||||
return (
|
||||
<html lang={locale} className={`${geistSans.variable} ${geistMono.variable} ${instrumentSans.variable} h-full antialiased`} suppressHydrationWarning>
|
||||
<head>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://unicons.iconscout.com/release/v4.0.0/css/line.css"
|
||||
/>
|
||||
</head>
|
||||
<body className="min-h-full flex flex-col" suppressHydrationWarning>
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
{children}
|
||||
</NextIntlClientProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { Navbar } from '@/components/sections/Navbar';
|
||||
import { Hero } from '@/components/sections/Hero';
|
||||
import { Why } from '@/components/sections/Why';
|
||||
import { Ecosystem } from '@/components/sections/Ecosystem';
|
||||
import { Destinations } from '@/components/sections/Destinations';
|
||||
import { Dashboard } from '@/components/sections/Dashboard';
|
||||
import { Pricing } from '@/components/sections/Pricing';
|
||||
import { Contact } from '@/components/sections/Contact';
|
||||
import { Footer } from '@/components/sections/Footer';
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<main>
|
||||
{/* ============ FIXED NAV ============ */}
|
||||
<Navbar />
|
||||
|
||||
{/* ============ HERO ============ */}
|
||||
<Hero />
|
||||
|
||||
{/* ============ WHY SECTION ============ */}
|
||||
<Why />
|
||||
|
||||
{/* ============ ECOSYSTEM / BENTO ============ */}
|
||||
<Ecosystem />
|
||||
|
||||
{/* ============ DESTINATIONS (carousel) ============ */}
|
||||
<Destinations />
|
||||
|
||||
{/* ============ DASHBOARD ============ */}
|
||||
<Dashboard />
|
||||
|
||||
{/* ============ PRICING ============ */}
|
||||
<Pricing />
|
||||
|
||||
{/* ============ CONTACT ============ */}
|
||||
<Contact />
|
||||
|
||||
{/* ============ FOOTER ============ */}
|
||||
<Footer />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,227 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
export function DestinationsCarousel({
|
||||
header,
|
||||
children
|
||||
}: {
|
||||
header: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const viewportRef = useRef<HTMLDivElement>(null);
|
||||
const trackRef = useRef<HTMLDivElement>(null);
|
||||
const dotsRef = useRef<HTMLDivElement>(null);
|
||||
const prevBtnRef = useRef<HTMLButtonElement>(null);
|
||||
const nextBtnRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const viewport = viewportRef.current;
|
||||
const track = trackRef.current;
|
||||
const dotsWrap = dotsRef.current;
|
||||
if (!track || !viewport || !dotsWrap) return;
|
||||
|
||||
const realCards = Array.from(track.children) as HTMLElement[];
|
||||
const n = realCards.length;
|
||||
if (n === 0) return;
|
||||
|
||||
const CARD_WIDTH = 200;
|
||||
const ACTIVE_WIDTH = 320;
|
||||
const GAP = 16;
|
||||
let isAnimating = false;
|
||||
|
||||
function realIndexOf(pos: number) {
|
||||
return ((pos % n) + n) % n;
|
||||
}
|
||||
|
||||
function widthOfSlot(i: number, activeReal: number) {
|
||||
return realIndexOf(i) === activeReal ? ACTIVE_WIDTH : CARD_WIDTH;
|
||||
}
|
||||
|
||||
dotsWrap.innerHTML = "";
|
||||
const dots: HTMLButtonElement[] = [];
|
||||
realCards.forEach((c, i) => {
|
||||
const d = document.createElement("button");
|
||||
d.setAttribute("aria-label", "Ke slide " + (i + 1));
|
||||
d.addEventListener("click", () => {
|
||||
goTo(i);
|
||||
resetAutoplay();
|
||||
});
|
||||
dotsWrap.appendChild(d);
|
||||
dots.push(d);
|
||||
});
|
||||
|
||||
const beforeClones = realCards.map((c) => {
|
||||
const cl = c.cloneNode(true) as HTMLElement;
|
||||
cl.setAttribute("aria-hidden", "true");
|
||||
return cl;
|
||||
});
|
||||
const afterClones = realCards.map((c) => {
|
||||
const cl = c.cloneNode(true) as HTMLElement;
|
||||
cl.setAttribute("aria-hidden", "true");
|
||||
return cl;
|
||||
});
|
||||
|
||||
track.innerHTML = "";
|
||||
beforeClones.concat(realCards, afterClones).forEach((el) => {
|
||||
track.appendChild(el);
|
||||
});
|
||||
|
||||
const slots = Array.from(track.children) as HTMLElement[];
|
||||
let position = n + 2;
|
||||
|
||||
function render(instant: boolean) {
|
||||
if (instant) track!.classList.add("no-anim");
|
||||
else track!.classList.remove("no-anim");
|
||||
|
||||
const activeReal = realIndexOf(position);
|
||||
|
||||
let offset = 0;
|
||||
for (let i = 0; i < position; i++) {
|
||||
offset += widthOfSlot(i, activeReal) + GAP;
|
||||
}
|
||||
offset += widthOfSlot(position, activeReal) / 2;
|
||||
|
||||
const viewportRect = viewport!.getBoundingClientRect();
|
||||
const translate = viewportRect.width / 2 - offset;
|
||||
track!.style.transform = "translateX(" + translate + "px)";
|
||||
|
||||
slots.forEach((s, i) => {
|
||||
const isActive = realIndexOf(i) === activeReal;
|
||||
s.classList.toggle("active", isActive);
|
||||
});
|
||||
dots.forEach((d, i) => {
|
||||
d.classList.toggle("on", i === activeReal);
|
||||
});
|
||||
|
||||
if (instant) {
|
||||
void track!.offsetWidth;
|
||||
track!.classList.remove("no-anim");
|
||||
}
|
||||
}
|
||||
|
||||
function goToPosition(pos: number) {
|
||||
position = pos;
|
||||
isAnimating = true;
|
||||
render(false);
|
||||
}
|
||||
|
||||
function next() {
|
||||
if (isAnimating) return;
|
||||
goToPosition(position + 1);
|
||||
}
|
||||
|
||||
function prev() {
|
||||
if (isAnimating) return;
|
||||
goToPosition(position - 1);
|
||||
}
|
||||
|
||||
function goTo(realIndex: number) {
|
||||
if (isAnimating) return;
|
||||
goToPosition(n + realIndex);
|
||||
}
|
||||
|
||||
const onTransitionEnd = (e: TransitionEvent) => {
|
||||
if (e.target !== track || e.propertyName !== "transform") return;
|
||||
if (position >= 2 * n) {
|
||||
position -= n;
|
||||
render(true);
|
||||
} else if (position < n) {
|
||||
position += n;
|
||||
render(true);
|
||||
}
|
||||
isAnimating = false;
|
||||
};
|
||||
track.addEventListener("transitionend", onTransitionEnd);
|
||||
|
||||
const onPrevClick = () => { prev(); resetAutoplay(); };
|
||||
const onNextClick = () => { next(); resetAutoplay(); };
|
||||
|
||||
const prevBtn = prevBtnRef.current;
|
||||
const nextBtn = nextBtnRef.current;
|
||||
if (prevBtn) prevBtn.addEventListener("click", onPrevClick);
|
||||
if (nextBtn) nextBtn.addEventListener("click", onNextClick);
|
||||
|
||||
const onTrackClick = (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
const card = target.closest('.dest-card') as HTMLElement;
|
||||
if (!card || isAnimating) return;
|
||||
|
||||
const slotsArray = Array.from(track.children) as HTMLElement[];
|
||||
const slotIndex = slotsArray.indexOf(card);
|
||||
|
||||
if (slotIndex !== -1) {
|
||||
goToPosition(slotIndex);
|
||||
resetAutoplay();
|
||||
}
|
||||
};
|
||||
track.addEventListener("click", onTrackClick);
|
||||
|
||||
let autoplayTimer: ReturnType<typeof setInterval>;
|
||||
function startAutoplay() {
|
||||
autoplayTimer = setInterval(next, 5000);
|
||||
}
|
||||
function stopAutoplay() {
|
||||
clearInterval(autoplayTimer);
|
||||
}
|
||||
function resetAutoplay() {
|
||||
stopAutoplay();
|
||||
startAutoplay();
|
||||
}
|
||||
viewport.addEventListener("mouseenter", stopAutoplay);
|
||||
viewport.addEventListener("mouseleave", startAutoplay);
|
||||
|
||||
const onResize = () => render(true);
|
||||
window.addEventListener("resize", onResize);
|
||||
|
||||
render(true);
|
||||
startAutoplay();
|
||||
|
||||
return () => {
|
||||
stopAutoplay();
|
||||
window.removeEventListener("resize", onResize);
|
||||
track.removeEventListener("transitionend", onTransitionEnd);
|
||||
track.removeEventListener("click", onTrackClick);
|
||||
viewport.removeEventListener("mouseenter", stopAutoplay);
|
||||
viewport.removeEventListener("mouseleave", startAutoplay);
|
||||
if (prevBtn) prevBtn.removeEventListener("click", onPrevClick);
|
||||
if (nextBtn) nextBtn.removeEventListener("click", onNextClick);
|
||||
|
||||
track.innerHTML = "";
|
||||
realCards.forEach((c) => track.appendChild(c));
|
||||
dotsWrap.innerHTML = "";
|
||||
};
|
||||
}, [header, children]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="destinations-head-row">
|
||||
{header}
|
||||
<div className="dest-controls">
|
||||
<button
|
||||
className="dest-btn"
|
||||
ref={prevBtnRef}
|
||||
aria-label="Slide sebelumnya"
|
||||
>
|
||||
<i className="uil uil-angle-left"></i>
|
||||
</button>
|
||||
<button
|
||||
className="dest-btn"
|
||||
ref={nextBtnRef}
|
||||
aria-label="Slide berikutnya"
|
||||
>
|
||||
<i className="uil uil-angle-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="dest-viewport" ref={viewportRef}>
|
||||
<div className="dest-track" ref={trackRef}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="dest-dots" ref={dotsRef}></div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { usePathname, useRouter } from '@/i18n/routing';
|
||||
import { useLocale } from 'next-intl';
|
||||
import { ChangeEvent } from 'react';
|
||||
|
||||
export function LanguageSwitcher() {
|
||||
const locale = useLocale();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
function onSelectChange(event: ChangeEvent<HTMLSelectElement>) {
|
||||
const nextLocale = event.target.value;
|
||||
router.replace(pathname, { locale: nextLocale });
|
||||
}
|
||||
|
||||
return (
|
||||
<select
|
||||
defaultValue={locale}
|
||||
onChange={onSelectChange}
|
||||
className="bg-transparent border border-white/20 rounded-md text-white px-3 py-2.5 text-sm focus:outline-none focus:border-brand-2-500 appearance-none cursor-pointer hover:bg-white/5 transition-colors"
|
||||
style={{
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E")`,
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: 'right 10px center',
|
||||
paddingRight: '32px',
|
||||
fontFamily: 'var(--font-body)',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
<option value="id" className="text-black font-semibold">
|
||||
ID
|
||||
</option>
|
||||
<option value="en" className="text-black font-semibold">
|
||||
EN
|
||||
</option>
|
||||
</select>
|
||||
);
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useTranslations, useLocale } from 'next-intl';
|
||||
import { brandInfo } from '@repo/brand';
|
||||
|
||||
export function Contact() {
|
||||
const t = useTranslations();
|
||||
const locale = useLocale() as 'id' | 'en';
|
||||
|
||||
const contactList = [
|
||||
{
|
||||
icon: 'uil-envelope-alt',
|
||||
titleKey: 'Contact.email',
|
||||
value: brandInfo.email,
|
||||
},
|
||||
{
|
||||
icon: 'uil-phone',
|
||||
titleKey: 'Contact.phone',
|
||||
value: brandInfo.phone,
|
||||
},
|
||||
{
|
||||
icon: 'uil-map-marker',
|
||||
titleKey: 'Contact.office',
|
||||
value: brandInfo.address[locale] || brandInfo.address.id,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="contact" id="contact">
|
||||
<div className="container">
|
||||
<div className="contact-card">
|
||||
<div className="contact-info">
|
||||
<span className="eyebrow eyebrow-light eyebrow-pill">
|
||||
<span className="dot"></span>
|
||||
{t('Contact.eyebrow')}
|
||||
</span>
|
||||
<h2 style={{ marginTop: '14px' }}>{t('Contact.title')}</h2>
|
||||
<p>{t('Contact.desc')}</p>
|
||||
|
||||
<div className="contact-list">
|
||||
{contactList.map((item, i) => (
|
||||
<div className="contact-item" key={i}>
|
||||
<div className="ci-icon">
|
||||
<i className={`uil ${item.icon}`}></i>
|
||||
</div>
|
||||
<div>
|
||||
<b>{(t as any)(item.titleKey)}</b>
|
||||
<span>{item.value}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="contact-form">
|
||||
<h3>{t('Contact.form.title')}</h3>
|
||||
<form onSubmit={(e) => e.preventDefault()}>
|
||||
<div className="field-row">
|
||||
<div className="field">
|
||||
<label htmlFor="cname">{t('Contact.form.name')}</label>
|
||||
<input id="cname" type="text" placeholder={t('Contact.form.placeholderName')} />
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="cemail">{t('Contact.email')}</label>
|
||||
<input id="cemail" type="email" placeholder={t('Contact.form.placeholderEmail')} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="cmsg">{t('Contact.form.needs')}</label>
|
||||
<textarea id="cmsg" placeholder={t('Contact.form.placeholderNeeds')}></textarea>
|
||||
</div>
|
||||
<button type="submit" className="btn btn-orange">
|
||||
{t('Contact.form.submit')}
|
||||
</button>
|
||||
<p className="form-note">{t('Contact.form.note')}</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export function Dashboard() {
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
<section className="dash-showcase" id="dashboard">
|
||||
<div className="container">
|
||||
<div className="dash-head">
|
||||
<h2>
|
||||
{t('Destinations.title21')}
|
||||
<br />
|
||||
{t('Destinations.title22')}
|
||||
</h2>
|
||||
<p>{t('Destinations.desc2')}</p>
|
||||
</div>
|
||||
|
||||
<svg width="0" height="0" style={{ position: 'absolute' }}>
|
||||
<defs>
|
||||
<linearGradient id="flowGrad" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" style={{ stopColor: 'var(--navy-900)' }} />
|
||||
<stop offset="100%" style={{ stopColor: 'var(--color-brand-2-500)' }} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<div className="dash-flow">
|
||||
<div className="dash-flow-row">
|
||||
<div className="dash-source-card wide">
|
||||
<div className="dash-source-icon">
|
||||
<i className="uil uil-calendar-alt"></i>
|
||||
</div>
|
||||
<h4>{t('Destinations.apps.online.title')}</h4>
|
||||
<p>{t('Destinations.apps.online.desc')}</p>
|
||||
</div>
|
||||
|
||||
<div className="dash-source-card wide">
|
||||
<div className="dash-source-icon">
|
||||
<i className="uil uil-money-stack"></i>
|
||||
</div>
|
||||
<h4>{t('Destinations.apps.cashier.title')}</h4>
|
||||
<p>{t('Destinations.apps.cashier.desc')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="dash-flow-merge">
|
||||
<svg className="flow-curve" viewBox="0 0 60 100" preserveAspectRatio="none">
|
||||
<path className="flow-dash" d="M0,24 C 32,24 20,50 58,50" />
|
||||
<path className="flow-dash" d="M0,76 C 32,76 20,50 58,50" />
|
||||
</svg>
|
||||
<span className="flow-simple">
|
||||
<i className="uil uil-angle-double-down"></i>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="dash-source-card wide">
|
||||
<div className="dash-source-icon">
|
||||
<i className="uil uil-newspaper"></i>
|
||||
</div>
|
||||
<h4>{t('Destinations.apps.pos.title')}</h4>
|
||||
<p>{t('Destinations.apps.pos.desc')}</p>
|
||||
</div>
|
||||
|
||||
<div className="dash-flow-arrow">
|
||||
<svg className="flow-curve" viewBox="0 0 44 20" preserveAspectRatio="none">
|
||||
<path className="flow-dash" d="M1,10 C 14,1 20,19 43,11" />
|
||||
</svg>
|
||||
<span className="flow-simple">
|
||||
<i className="uil uil-angle-double-down"></i>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="dash-source-card wide">
|
||||
<div className="dash-source-icon">
|
||||
<i className="uil uil-users-alt"></i>
|
||||
</div>
|
||||
<h4>{t('Destinations.apps.queue.title')}</h4>
|
||||
<p>{t('Destinations.apps.queue.desc')}</p>
|
||||
</div>
|
||||
|
||||
<div className="dash-flow-arrow">
|
||||
<svg className="flow-curve" viewBox="0 0 44 20" preserveAspectRatio="none">
|
||||
<path className="flow-dash" d="M1,10 C 14,1 20,19 43,11 " />
|
||||
</svg>
|
||||
<span className="flow-simple">
|
||||
<i className="uil uil-angle-double-down"></i>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="dash-source-card wide accent">
|
||||
<div className="dash-source-icon">
|
||||
<i className="uil uil-desktop"></i>
|
||||
</div>
|
||||
<h4>{t('Destinations.apps.admin.title')}</h4>
|
||||
<p>{t('Destinations.apps.admin.desc')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { DestinationsCarousel } from '@/components/DestinationsCarousel';
|
||||
|
||||
export function Destinations() {
|
||||
const t = useTranslations();
|
||||
|
||||
const destinationsData = [
|
||||
{
|
||||
icon: 'uil-trees',
|
||||
titleKey: 'Destinations.items.themePark.title',
|
||||
descKey: 'Destinations.items.themePark.desc',
|
||||
},
|
||||
{
|
||||
icon: 'uil-ticket',
|
||||
titleKey: 'Destinations.items.event.title',
|
||||
descKey: 'Destinations.items.event.desc',
|
||||
},
|
||||
{
|
||||
icon: 'uil-archway',
|
||||
titleKey: 'Destinations.items.museum.title',
|
||||
descKey: 'Destinations.items.museum.desc',
|
||||
},
|
||||
{
|
||||
icon: 'uil-basketball',
|
||||
titleKey: 'Destinations.items.stadium.title',
|
||||
descKey: 'Destinations.items.stadium.desc',
|
||||
},
|
||||
{
|
||||
icon: 'uil-scenery',
|
||||
titleKey: 'Destinations.items.exhibition.title',
|
||||
descKey: 'Destinations.items.exhibition.desc',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="destinations" id="destinations">
|
||||
<div className="container">
|
||||
<DestinationsCarousel
|
||||
header={
|
||||
<div className="destinations-head">
|
||||
<h2>{t('Destinations.title')}</h2>
|
||||
<p>{t('Destinations.desc')}</p>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{destinationsData.map((item, i) => (
|
||||
<div className="dest-card" key={i}>
|
||||
<h4>{(t as any)(item.titleKey)}</h4>
|
||||
<p>{(t as any)(item.descKey)}</p>
|
||||
<div className="dest-icon">
|
||||
<i className={`uil ${item.icon}`}></i>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</DestinationsCarousel>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,208 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export function Ecosystem() {
|
||||
const t = useTranslations();
|
||||
|
||||
useEffect(() => {
|
||||
const grid = document.getElementById('bentoGrid');
|
||||
let obs: IntersectionObserver;
|
||||
|
||||
if (grid) {
|
||||
const bars = grid.querySelectorAll<HTMLElement>('.report-bar');
|
||||
const ring = grid.querySelector<SVGElement>('.ring-progress');
|
||||
const timeouts: ReturnType<typeof setTimeout>[] = [];
|
||||
|
||||
obs = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
bars.forEach((bar, i) => {
|
||||
timeouts.push(
|
||||
setTimeout(() => {
|
||||
if (bar.dataset.w) bar.style.width = bar.dataset.w;
|
||||
}, i * 150)
|
||||
);
|
||||
});
|
||||
if (ring && ring.dataset.dash) {
|
||||
timeouts.push(
|
||||
setTimeout(() => {
|
||||
ring.style.strokeDasharray = ring.dataset.dash!;
|
||||
}, 200)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Clear pending timeouts
|
||||
timeouts.forEach(clearTimeout);
|
||||
timeouts.length = 0;
|
||||
|
||||
// Reset elements to trigger transition again on next enter
|
||||
bars.forEach((bar) => {
|
||||
bar.style.width = '0';
|
||||
});
|
||||
if (ring) {
|
||||
ring.style.strokeDasharray = '0 213.6';
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold: 0.35 },
|
||||
);
|
||||
obs.observe(grid);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (obs) obs.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className="ecosystem" id="ecosystem">
|
||||
<div className="container">
|
||||
<div className="ecosystem-head">
|
||||
<h2>{t('Ecosystem.title')}</h2>
|
||||
<p>{t('Ecosystem.desc')}</p>
|
||||
</div>
|
||||
|
||||
<div className="bento" id="bentoGrid">
|
||||
{/* Ticketing */}
|
||||
<div className="bento-card bento-r1">
|
||||
<h3>{t('Ecosystem.ticketing.title')}</h3>
|
||||
<p>{t('Ecosystem.ticketing.desc')}</p>
|
||||
<div className="bento-illustration">
|
||||
<div className="ticket-rows">
|
||||
<div className="ticket-row">
|
||||
<div className="ticket-track drift-a" style={{ marginLeft: '-14px' }}>
|
||||
<span className="pill pill-outline">TX24409198-8798</span>
|
||||
<span className="pill pill-orange">TX24409198-8797</span>
|
||||
<span className="pill pill-lime">TX24409198-8798</span>
|
||||
<span className="pill pill-outline">TX24409198-8901</span>
|
||||
<span className="pill pill-navy">TX24409198-8811</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ticket-row">
|
||||
<div className="ticket-track drift-b" style={{ marginLeft: '-32px' }}>
|
||||
<span className="pill pill-lime">TX24409198-8801</span>
|
||||
<span className="pill pill-outline">TX24409198-8787</span>
|
||||
<span className="pill pill-navy">TX24409198-8786</span>
|
||||
<span className="pill pill-outline">TX24409198-8901</span>
|
||||
<span className="pill pill-outline">TX24409198-8881</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ticket-row">
|
||||
<div className="ticket-track drift-a" style={{ marginLeft: '-22px', animationDelay: '-1.4s' }}>
|
||||
<span className="pill pill-outline">TX24409198-8902</span>
|
||||
<span className="pill pill-lime">TX24409198-8788</span>
|
||||
<span className="pill pill-outline">TX24409198-8790</span>
|
||||
<span className="pill pill-orange">TX24409198-8905</span>
|
||||
<span className="pill pill-outline">TX24409198-8812</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Access Management */}
|
||||
<div className="bento-card bento-r2">
|
||||
<div className="bento-illustration access-illustration">
|
||||
<div className="chip-row">
|
||||
<div className="chip active">
|
||||
<span>{t('Ecosystem.stats.qr')}</span>
|
||||
<i className="uil uil-qrcode-scan"></i>
|
||||
</div>
|
||||
<div className="chip">
|
||||
<span>{t('Ecosystem.stats.gate')}</span>
|
||||
<i className="uil uil-monitor-heart-rate"></i>
|
||||
</div>
|
||||
<div className="chip">
|
||||
<span>{t('Ecosystem.stats.rfid')}</span>
|
||||
<i className="uil uil-rss-interface"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ring-wrap">
|
||||
<svg width="140" height="140" viewBox="0 0 100 100">
|
||||
<defs>
|
||||
<linearGradient id="rainbowGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stopColor="#f66b0e" />
|
||||
<stop offset="70%" stopColor="#112b3c" />
|
||||
<stop offset="100%" stopColor="#205375" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle cx="50" cy="50" r="34" fill="none" stroke="#E1E5E3" strokeWidth="9" />
|
||||
<circle
|
||||
className="ring-progress"
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="34"
|
||||
fill="none"
|
||||
stroke="url(#rainbowGrad)"
|
||||
strokeWidth="9"
|
||||
strokeLinecap="round"
|
||||
data-dash="166 213.6"
|
||||
transform="rotate(-90 50 50)"
|
||||
/>
|
||||
</svg>
|
||||
<div className="ring-text">
|
||||
<b>0.4s</b>
|
||||
<small>{t('Ecosystem.stats.avg')}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h3>{t('Ecosystem.access.title')}</h3>
|
||||
<p>{t('Ecosystem.access.desc')}</p>
|
||||
</div>
|
||||
|
||||
{/* Visitor Operations */}
|
||||
<div className="bento-card bento-r3">
|
||||
<div className="bento-illustration visitor-illustration">
|
||||
<div className="avatar-row">
|
||||
<div className="avatar-stack">
|
||||
<div className="avatar" style={{ background: '#b9f001' }}></div>
|
||||
<div className="avatar" style={{ background: '#f66b0e' }}></div>
|
||||
<div className="avatar" style={{ background: '#112b3c' }}></div>
|
||||
<div className="avatar" style={{ background: '#e1e5e3' }}></div>
|
||||
</div>
|
||||
<span>{t('Ecosystem.stats.queue')}</span>
|
||||
</div>
|
||||
<div className="zone-grid">
|
||||
<div className="zone-cell" style={{ background: '#e1e5e3' }}></div>
|
||||
<div className="zone-cell" style={{ background: '#e1e5e3' }}></div>
|
||||
<div className="zone-cell" style={{ background: '#205375' }}></div>
|
||||
<div className="zone-cell" style={{ background: '#e1e5e3' }}></div>
|
||||
<div className="zone-cell" style={{ background: '#e1e5e3' }}></div>
|
||||
<div className="zone-cell" style={{ background: '#b9f001' }}></div>
|
||||
<div className="zone-cell" style={{ background: '#e1e5e3' }}></div>
|
||||
<div className="zone-cell" style={{ background: 'linear-gradient(135deg, #205375, #112b3c)' }}></div>
|
||||
</div>
|
||||
</div>
|
||||
<h3>{t('Ecosystem.visitor.title')}</h3>
|
||||
<p>{t('Ecosystem.visitor.desc')}</p>
|
||||
</div>
|
||||
|
||||
{/* Report */}
|
||||
<div className="bento-card bento-r4">
|
||||
<h3 style={{ marginBottom: '8px' }}>{t('Ecosystem.report.title')}</h3>
|
||||
<p style={{ marginBottom: '26px' }}>{t('Ecosystem.report.desc')}</p>
|
||||
<div className="report-bars">
|
||||
{[
|
||||
{ w: '150px', bg: '#b9f001', val: 'Rp32.5M', key: 'Ecosystem.stats.revenue' },
|
||||
{ w: '220px', bg: '#f66b0e', val: '1,284', key: 'Ecosystem.stats.visitors' },
|
||||
{ w: '300px', bg: '#112b3c', val: '1,120,240', key: 'Ecosystem.stats.checkin' },
|
||||
].map((item, idx) => (
|
||||
<div className="report-bar-row" key={idx}>
|
||||
<div className="report-bar" data-w={item.w} style={{ background: item.bg }}></div>
|
||||
<div className="report-bar-value">
|
||||
<b>{item.val}</b>
|
||||
<small>{(t as any)(item.key)}</small>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import { useTranslations, useLocale } from 'next-intl';
|
||||
import { brandInfo } from '@repo/brand';
|
||||
|
||||
export function Footer() {
|
||||
const t = useTranslations();
|
||||
const locale = useLocale() as 'id' | 'en';
|
||||
|
||||
const footerLinks = [
|
||||
{ labelKey: 'Nav.home', href: '#' },
|
||||
{ labelKey: 'Nav.solutions', href: '#ecosystem' },
|
||||
{ labelKey: 'Nav.industries', href: '#destinations' },
|
||||
{ labelKey: 'Nav.pricing', href: '#pricing' },
|
||||
{ labelKey: 'Nav.contact', href: '#contact' },
|
||||
];
|
||||
|
||||
const socialLinks = [
|
||||
{ icon: 'uil-envelope-alt', href: brandInfo.socialLinks.email },
|
||||
{ icon: 'uil-phone', href: brandInfo.socialLinks.phone },
|
||||
{ icon: 'uil-youtube', href: brandInfo.socialLinks.youtube },
|
||||
];
|
||||
|
||||
return (
|
||||
<footer className="footer">
|
||||
<div className="container">
|
||||
<div className="footer-top">
|
||||
<div className="footer-brand">
|
||||
<div className="brand">
|
||||
<a href="#">
|
||||
<div className="brand-mark">
|
||||
<img src="/brand/logo-primary.svg" alt="" />
|
||||
</div>
|
||||
</a>
|
||||
<span className="brand-name" style={{ color: '#fff' }}></span>
|
||||
</div>
|
||||
<p>{t('Footer.desc')}</p>
|
||||
<p className="address">{brandInfo.address[locale] || brandInfo.address.id}</p>
|
||||
</div>
|
||||
<div className="footer-cols">
|
||||
<div className="footer-col">
|
||||
<h5>{t('Footer.links')}</h5>
|
||||
{footerLinks.map((link, i) => (
|
||||
<a href={link.href} key={i}>
|
||||
{(t as any)(link.labelKey)}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
<div className="footer-col">
|
||||
<h5>{t('Nav.contact')}</h5>
|
||||
<a href={`mailto:${brandInfo.email}`}>{brandInfo.email}</a>
|
||||
<a href={`tel:${brandInfo.phone.replace(/[\s-]/g, '')}`}>{brandInfo.phone}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="footer-bottom">
|
||||
<p>
|
||||
© <span>{new Date().getFullYear()}</span> {t('Footer.copyright')}
|
||||
</p>
|
||||
<div className="footer-social">
|
||||
{socialLinks.map((social, i) => (
|
||||
<a href={social.href} key={i}>
|
||||
<i className={`uil ${social.icon}`}></i>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export function Hero() {
|
||||
const t = useTranslations();
|
||||
|
||||
const statsData = [
|
||||
{
|
||||
icon: 'uil-clock-eight',
|
||||
labelKey: 'Hero.stats.timeAccess',
|
||||
value: (
|
||||
<>
|
||||
0.4<span>{t('Hero.stats.seconds')}</span>
|
||||
</>
|
||||
),
|
||||
subKey: 'Hero.stats.avgValidation',
|
||||
},
|
||||
{
|
||||
icon: 'uil-ticket',
|
||||
labelKey: 'Hero.stats.ticketsSold',
|
||||
value: '1.249',
|
||||
subKey: 'Hero.stats.todayAllChannels',
|
||||
},
|
||||
{
|
||||
icon: 'uil-sync',
|
||||
labelKey: 'Hero.stats.sync',
|
||||
value: '12',
|
||||
subKey: 'Hero.stats.txSynced',
|
||||
},
|
||||
{
|
||||
icon: 'uil-sliders-v',
|
||||
labelKey: 'Hero.stats.visitorOps',
|
||||
bars: [40, 70, 55, 90],
|
||||
subKey: 'Hero.stats.activeZones',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<header className="hero">
|
||||
<div className="container">
|
||||
<div className="hero-inner">
|
||||
<h1>
|
||||
{t('Hero.title1')}
|
||||
<br />
|
||||
{t('Hero.title2')}
|
||||
</h1>
|
||||
|
||||
<div className="stat-row">
|
||||
{statsData.map((stat, i) => (
|
||||
<div className="stat-card" key={i}>
|
||||
<div className="stat-icon">
|
||||
<i className={`uil ${stat.icon}`}></i>
|
||||
</div>
|
||||
<div className="stat-label">{(t as any)(stat.labelKey)}</div>
|
||||
|
||||
{stat.bars ? (
|
||||
<div className="stat-bars" style={{ marginTop: '12px' }}>
|
||||
{stat.bars.map((h, j) => (
|
||||
<span key={j} style={{ height: `${h}%` }}></span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="stat-value">{stat.value}</div>
|
||||
)}
|
||||
|
||||
<div className="stat-sub">{(t as any)(stat.subKey)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="hero-body">
|
||||
<span className="eyebrow eyebrow-light eyebrow-pill">
|
||||
<span className="dot"></span>
|
||||
{t('Hero.subtitle')}
|
||||
</span>
|
||||
<p>{t('Hero.desc')}</p>
|
||||
<a href="#why" className="btn btn-navy">
|
||||
{t('Hero.cta')}
|
||||
<i className="uil uil-arrow-down-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { LanguageSwitcher } from '@/components/LanguageSwitcher';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export function Navbar() {
|
||||
const t = useTranslations();
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
const floating = window.scrollY > 60;
|
||||
setIsScrolled(floating);
|
||||
document.body.classList.toggle('nav-is-floating', floating);
|
||||
};
|
||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||
handleScroll();
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={`nav-wrapper ${isScrolled ? 'floating' : ''}`}>
|
||||
<div className="container">
|
||||
<nav className="nav">
|
||||
<div className="brand">
|
||||
<a href="#">
|
||||
<div className="brand-mark">
|
||||
<img src="/brand/logo-primary.svg" alt="" />
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<button className="nav-toggle" aria-label="Open menu" onClick={() => setIsMenuOpen(!isMenuOpen)}>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M4 7h16M4 12h16M4 17h16" stroke="#fff" strokeWidth="2" strokeLinecap="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<div className={`nav-links ${isMenuOpen ? 'open' : ''}`} id="navLinks" onClick={() => setIsMenuOpen(false)}>
|
||||
<a href="#" style={{ color: '#fff', opacity: 0.85 }}>
|
||||
{t('Nav.home')}
|
||||
</a>
|
||||
<a href="#ecosystem" style={{ color: '#fff', opacity: 0.85 }}>
|
||||
{t('Nav.solutions')}
|
||||
</a>
|
||||
<a href="#destinations" style={{ color: '#fff', opacity: 0.85 }}>
|
||||
{t('Nav.industries')}
|
||||
</a>
|
||||
<a href="#pricing" style={{ color: '#fff', opacity: 0.85 }}>
|
||||
{t('Nav.pricing')}
|
||||
</a>
|
||||
<a href="#contact" style={{ color: '#fff', opacity: 0.85 }}>
|
||||
{t('Nav.contact')}
|
||||
</a>
|
||||
<a href="#contact" className="btn btn-orange">
|
||||
{t('Nav.demo')}
|
||||
</a>
|
||||
<div className="nav-divider"></div>
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export function Pricing() {
|
||||
const t = useTranslations();
|
||||
|
||||
const pricingPlans = [
|
||||
{
|
||||
key: 'rental',
|
||||
tags: [0, 1, 2],
|
||||
features: ['f1', 'f2', 'f3', 'f4', 'f5'],
|
||||
},
|
||||
{
|
||||
key: 'subscription',
|
||||
tags: [0, 1],
|
||||
features: ['f1', 'f2', 'f3', 'f4', 'f5'],
|
||||
},
|
||||
{
|
||||
key: 'onPremise',
|
||||
tags: [0, 1],
|
||||
features: ['f1', 'f2', 'f3', 'f4', 'f5'],
|
||||
},
|
||||
{
|
||||
key: 'buyOut',
|
||||
tags: [0, 1, 2],
|
||||
features: ['f1', 'f2', 'f3', 'f4', 'f5'],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="pricing" id="pricing">
|
||||
<div className="container-lg">
|
||||
<div className="pricing-head">
|
||||
<h2>
|
||||
{t('Pricing.title1')}
|
||||
<br />
|
||||
{t('Pricing.title2')}
|
||||
</h2>
|
||||
<p>{t('Pricing.desc')}</p>
|
||||
</div>
|
||||
|
||||
<div className="pricing-grid">
|
||||
{pricingPlans.map((plan) => (
|
||||
<div className="price-card" key={plan.key}>
|
||||
<h3>{(t as any)(`Pricing.${plan.key}.title`)}</h3>
|
||||
<div className="price-tags">
|
||||
{plan.tags.map((tagIndex) => (
|
||||
<span className="tag" key={tagIndex}>
|
||||
{(t as any)(`Pricing.${plan.key}.tags.${tagIndex}`)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<p className="price-desc">{(t as any)(`Pricing.${plan.key}.desc`)}</p>
|
||||
<a href="#contact" className="btn btn-orange">
|
||||
{t('Pricing.contactUs')}
|
||||
</a>
|
||||
<span className="feat-label">{t('Pricing.whatYouGet')}</span>
|
||||
<ul className="feat-list">
|
||||
{plan.features.map((featKey) => (
|
||||
<li key={featKey}>
|
||||
<i className="uil uil-check-circle"></i>
|
||||
{(t as any)(`Pricing.${plan.key}.${featKey}`)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export function Why() {
|
||||
const t = useTranslations();
|
||||
|
||||
const reasonsData = [
|
||||
{
|
||||
icon: 'uil-wifi-slash',
|
||||
titleKey: 'Why.items.offline.title',
|
||||
descKey: 'Why.items.offline.desc',
|
||||
},
|
||||
{
|
||||
icon: 'uil-bolt',
|
||||
titleKey: 'Why.items.speed.title',
|
||||
descKey: 'Why.items.speed.desc',
|
||||
},
|
||||
{
|
||||
icon: 'uil-expand-arrows-alt',
|
||||
titleKey: 'Why.items.flexibility.title',
|
||||
descKey: 'Why.items.flexibility.desc',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="why" id="why">
|
||||
<div className="container why-grid">
|
||||
<div className="why-sticky">
|
||||
<h2>{t('Why.title')}</h2>
|
||||
<p>{t('Why.desc')}</p>
|
||||
<div className="why-arrow">
|
||||
<i className="uil uil-arrow-up-right"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="why-cards">
|
||||
{reasonsData.map((reason, i) => (
|
||||
<div className="why-card" key={i}>
|
||||
<div className="why-card-icon">
|
||||
<i className={`uil ${reason.icon}`}></i>
|
||||
</div>
|
||||
<h3>{(t as any)(reason.titleKey)}</h3>
|
||||
<p>{(t as any)(reason.descKey)}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import {getRequestConfig} from 'next-intl/server';
|
||||
import {routing} from './routing';
|
||||
|
||||
export default getRequestConfig(async ({requestLocale}) => {
|
||||
let locale = await requestLocale;
|
||||
|
||||
if (!locale || !routing.locales.includes(locale as any)) {
|
||||
locale = routing.defaultLocale;
|
||||
}
|
||||
|
||||
return {
|
||||
locale,
|
||||
messages: (await import(`../../messages/${locale}.json`)).default
|
||||
};
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
import {defineRouting} from 'next-intl/routing';
|
||||
import {createNavigation} from 'next-intl/navigation';
|
||||
|
||||
export const routing = defineRouting({
|
||||
locales: ['en', 'id'],
|
||||
defaultLocale: 'en'
|
||||
});
|
||||
|
||||
export const {Link, redirect, usePathname, useRouter} =
|
||||
createNavigation(routing);
|
||||
@@ -1,8 +0,0 @@
|
||||
import createMiddleware from 'next-intl/middleware';
|
||||
import {routing} from './i18n/routing';
|
||||
|
||||
export default createMiddleware(routing);
|
||||
|
||||
export const config = {
|
||||
matcher: ['/', '/(id|en)/:path*']
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"extends": "@repo/typescript-config/nextjs.json",
|
||||
"compilerOptions": {
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts",
|
||||
"**/*.mts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user