remove product-site application and its related assets from the repository
This commit is contained in:
@@ -2,26 +2,25 @@
|
|||||||
// This container standardizes the local development environment for the Monorepo.
|
// This container standardizes the local development environment for the Monorepo.
|
||||||
{
|
{
|
||||||
"name": "FE Monorepo Template",
|
"name": "FE Monorepo Template",
|
||||||
|
|
||||||
// Base image providing Node.js v24, TypeScript, and a Debian Bookworm environment.
|
// Base image providing Node.js v24, TypeScript, and a Debian Bookworm environment.
|
||||||
"image": "mcr.microsoft.com/devcontainers/typescript-node:24-bookworm",
|
"image": "mcr.microsoft.com/devcontainers/typescript-node:24-bookworm",
|
||||||
|
|
||||||
// DevContainer Features: Pre-installed tools needed for our workflow.
|
// DevContainer Features: Pre-installed tools needed for our workflow.
|
||||||
"features": {
|
"features": {
|
||||||
"ghcr.io/devcontainers/features/git:1": {},
|
"ghcr.io/devcontainers/features/git:1": {},
|
||||||
"ghcr.io/devcontainers-extra/features/pnpm:2": {},
|
"ghcr.io/devcontainers-extra/features/pnpm:2": {},
|
||||||
"ghcr.io/devcontainers/features/sshd:1": {}
|
"ghcr.io/devcontainers/features/sshd:1": {}
|
||||||
},
|
},
|
||||||
|
|
||||||
// ─── Port Topology ──────────────────────────────────────────────────────────
|
// ─── Port Topology ──────────────────────────────────────────────────────────
|
||||||
// 6060: VitePress Docs (Development)
|
// 6060: VitePress Docs (Development)
|
||||||
// 4173: VitePress Docs (Preview)
|
// 4173: VitePress Docs (Preview)
|
||||||
// 5173: Web App (Must be strictly 5173 for Electron IPC compatibility)
|
// 5173: Web App (Must be strictly 5173 for Electron IPC compatibility)
|
||||||
// 3000: Landing App (Isolated from Vite's default 517x blast radius)
|
// 3000: Landing App (Isolated from Vite's default 517x blast radius)
|
||||||
// 3001: Company Profile App
|
|
||||||
// 3005: Showcase App
|
// 3005: Showcase App
|
||||||
"forwardPorts": [6060, 4173, 5173, 3000, 3001, 3005],
|
"forwardPorts": [6060, 4173, 5173, 3000, 3005],
|
||||||
|
|
||||||
"portsAttributes": {
|
"portsAttributes": {
|
||||||
"6060": {
|
"6060": {
|
||||||
"label": "VitePress Docs",
|
"label": "VitePress Docs",
|
||||||
@@ -42,17 +41,13 @@
|
|||||||
"3000": {
|
"3000": {
|
||||||
"label": "Landing App (Public)",
|
"label": "Landing App (Public)",
|
||||||
"onAutoForward": "notify"
|
"onAutoForward": "notify"
|
||||||
},
|
|
||||||
"3001": {
|
|
||||||
"label": "Product Site App",
|
|
||||||
"onAutoForward": "notify"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// ─── Lifecycle Hooks ────────────────────────────────────────────────────────
|
// ─── Lifecycle Hooks ────────────────────────────────────────────────────────
|
||||||
// Automatically install all workspace dependencies once the container is built.
|
// Automatically install all workspace dependencies once the container is built.
|
||||||
"postCreateCommand": "pnpm install",
|
"postCreateCommand": "pnpm install",
|
||||||
|
|
||||||
// ─── Editor Standardization ─────────────────────────────────────────────────
|
// ─── Editor Standardization ─────────────────────────────────────────────────
|
||||||
// Enforces code style and tooling across all developers' machines.
|
// Enforces code style and tooling across all developers' machines.
|
||||||
"customizations": {
|
"customizations": {
|
||||||
@@ -62,18 +57,18 @@
|
|||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
},
|
},
|
||||||
"extensions": [
|
"extensions": [
|
||||||
"dbaeumer.vscode-eslint", // Linting
|
"dbaeumer.vscode-eslint", // Linting
|
||||||
"esbenp.prettier-vscode", // Formatting
|
"esbenp.prettier-vscode", // Formatting
|
||||||
"bradlc.vscode-tailwindcss", // Tailwind v4 IntelliSense
|
"bradlc.vscode-tailwindcss", // Tailwind v4 IntelliSense
|
||||||
"yoavbls.pretty-ts-errors", // Human-readable TypeScript errors
|
"yoavbls.pretty-ts-errors", // Human-readable TypeScript errors
|
||||||
"editorconfig.editorconfig" // Standardized indentation
|
"editorconfig.editorconfig" // Standardized indentation
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// ─── Security & Permissions ─────────────────────────────────────────────────
|
// ─── Security & Permissions ─────────────────────────────────────────────────
|
||||||
// CRITICAL: Run the container as 'node' instead of 'root'.
|
// CRITICAL: Run the container as 'node' instead of 'root'.
|
||||||
// This prevents 'Permission Denied' errors on the host OS when trying to
|
// This prevents 'Permission Denied' errors on the host OS when trying to
|
||||||
// delete or modify the 'node_modules' folder created inside the container.
|
// delete or modify the 'node_modules' folder created inside the container.
|
||||||
"remoteUser": "node"
|
"remoteUser": "node"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"]
|
|
||||||
}
|
|
||||||
@@ -9,14 +9,12 @@
|
|||||||
"build:docs-dev": "turbo run build --filter=docs-dev",
|
"build:docs-dev": "turbo run build --filter=docs-dev",
|
||||||
"build:desktop": "turbo run build --filter=desktop",
|
"build:desktop": "turbo run build --filter=desktop",
|
||||||
"build:showcase": "turbo run build --filter=showcase",
|
"build:showcase": "turbo run build --filter=showcase",
|
||||||
"build:product-site": "turbo run build --filter=product-site",
|
|
||||||
"dev": "turbo run dev",
|
"dev": "turbo run dev",
|
||||||
"dev:web": "turbo run dev --filter=web",
|
"dev:web": "turbo run dev --filter=web",
|
||||||
"dev:landing": "turbo run dev --filter=landing",
|
"dev:landing": "turbo run dev --filter=landing",
|
||||||
"dev:docs-dev": "turbo run dev --filter=docs-dev",
|
"dev:docs-dev": "turbo run dev --filter=docs-dev",
|
||||||
"dev:desktop": "turbo run dev --filter=web --filter=desktop --parallel",
|
"dev:desktop": "turbo run dev --filter=web --filter=desktop --parallel",
|
||||||
"dev:showcase": "turbo run dev --filter=showcase",
|
"dev:showcase": "turbo run dev --filter=showcase",
|
||||||
"dev:product-site": "turbo run dev --filter=product-site",
|
|
||||||
"package:desktop": "pnpm run build:desktop && pnpm --filter desktop run package",
|
"package:desktop": "pnpm run build:desktop && pnpm --filter desktop run package",
|
||||||
"package:mac": "pnpm run build:desktop && pnpm --filter desktop run package:mac",
|
"package:mac": "pnpm run build:desktop && pnpm --filter desktop run package:mac",
|
||||||
"package:win": "pnpm run build:desktop && pnpm --filter desktop run package:win",
|
"package:win": "pnpm run build:desktop && pnpm --filter desktop run package:win",
|
||||||
@@ -29,7 +27,6 @@
|
|||||||
"typecheck:landing": "turbo run typecheck --filter=landing",
|
"typecheck:landing": "turbo run typecheck --filter=landing",
|
||||||
"typecheck:desktop": "turbo run typecheck --filter=desktop",
|
"typecheck:desktop": "turbo run typecheck --filter=desktop",
|
||||||
"typecheck:showcase": "turbo run typecheck --filter=showcase",
|
"typecheck:showcase": "turbo run typecheck --filter=showcase",
|
||||||
"typecheck:product-site": "turbo run typecheck --filter=product-site",
|
|
||||||
"check:all": "turbo run lint typecheck test",
|
"check:all": "turbo run lint typecheck test",
|
||||||
"clean:workspaces": "rm -rf node_modules **/*/node_modules .turbo **/*/.turbo dist **/*/dist out **/*/out web-dist **/*/web-dist release **/*/release",
|
"clean:workspaces": "rm -rf node_modules **/*/node_modules .turbo **/*/.turbo dist **/*/dist out **/*/out web-dist **/*/web-dist release **/*/release",
|
||||||
"nuke": "pnpm run clean:workspaces && pnpm install && pnpm build"
|
"nuke": "pnpm run clean:workspaces && pnpm install && pnpm build"
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<svg width="86" height="95" viewBox="0 0 86 95" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<rect width="86" height="95" fill="#122D3F"/>
|
<!-- Blok Utama: Ungu lebih terang agar kontras di latar gelap -->
|
||||||
<path d="M51.8743 27.2083H35.0318C33.6609 27.2083 33.0734 28.4074 33.0734 29.1667V68.3353C33.0734 69.3145 34.0526 69.902 34.836 69.902H51.8743C53.3167 69.902 53.8327 69.3145 53.8327 68.3353V52.0803C53.8327 51.4928 54.2244 50.5136 55.5953 50.5136H57.9454C59.3163 50.5136 59.708 51.4928 59.708 52.0803V73.8189C59.708 74.9156 58.9246 75.7773 57.9454 75.7773H33.0734C32.29 75.7773 31.4414 75.255 31.115 74.9939C30.4622 74.4064 28.4515 72.4871 27.9815 71.8604C27.5115 71.2337 27.1981 70.8812 27.1981 69.902V23.2914C27.1981 22.1581 28.1773 21.333 29.1565 21.333H57.5537C59.3163 21.333 59.708 22.6308 59.708 23.2914V40.1339C59.708 41.2306 58.8341 42.0923 57.7495 42.0923H50.6992C49.7592 42.0923 49.1325 42.0923 48.5449 42.6799C48.2185 42.941 47.0971 44.1277 46.7824 44.4424C46.1948 45.03 46.3907 45.6175 46.7824 45.6175H49.1325C50.1117 45.6175 49.9158 46.4009 49.9158 46.4009V49.5344C49.9158 50.1219 49.72 50.3177 49.1325 50.3177H38.557C37.9694 50.3177 37.7736 50.1219 37.7736 49.5344V38.1755C37.7736 37.7055 37.9694 37.3921 38.557 37.3921H42.0821C42.7471 37.3921 42.8655 37.7838 42.8655 38.1755V40.5256C42.8655 40.9173 43.453 41.1131 44.0406 40.5256C44.7587 39.8075 46.3907 38.3713 47.174 37.588C47.7616 37.0004 48.088 37.0004 48.7408 37.0004H51.8743C53.173 37.0004 53.8327 35.8254 53.8327 35.042V29.1667C53.8327 28.1875 53.0493 27.2083 51.8743 27.2083Z" fill="#FF7B00"/>
|
<rect x="6" y="6" width="16" height="16" rx="4" fill="#8a57f3"/>
|
||||||
|
<!-- Blok Sekunder: Sedikit lebih terang -->
|
||||||
|
<rect x="18" y="18" width="16" height="16" rx="4" fill="#8a57f3" fill-opacity="0.8"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 374 B |
Binary file not shown.
|
Before Width: | Height: | Size: 2.3 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 8.3 KiB |
Generated
+154
-1776
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user