remove product-site application and its related assets from the repository

This commit is contained in:
Firman Ramdhani
2026-07-29 17:28:36 +07:00
parent cd41062ce7
commit c7ed85378b
34 changed files with 174 additions and 5646 deletions
-41
View File
@@ -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
-5
View File
@@ -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
View File
@@ -1 +0,0 @@
@AGENTS.md
-36
View File
@@ -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.
-3
View File
@@ -1,3 +0,0 @@
import { nextjsConfig } from "@repo/eslint-config/nextjs.mjs";
export default nextjsConfig;
-215
View File
@@ -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."
}
}
-215
View File
@@ -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."
}
}
-10
View File
@@ -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);
-32
View File
@@ -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"
}
}
-7
View File
@@ -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>
);
}
-15
View File
@@ -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
};
});
-10
View File
@@ -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);
-8
View File
@@ -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*']
};
-22
View File
@@ -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"]
}