Files
dticket.tootaio.com/app/pages/index/index.vue
xiaomai 4288c98e21 feat: setup Tailwind CSS and initial routing structure
Configure @tailwindcss/vite in Nuxt config
Add default layout and main CSS file
Create initial index and login pages
Replace default Nuxt welcome screen with page routing
2026-04-12 17:53:01 +08:00

184 lines
8.6 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="flex flex-col h-screen">
<div class="flex justify-between items-center px-6 pt-6 pb-2">
<span class="text-sm font-semibold tracking-wide text-stone-700 bg-stone-100/80 px-3 py-1.5 rounded-full">
Event Ticket System
</span>
<RouterLink to="/login" id="loginBtn"
class="text-sm shadow-md shadow-slate-900/20 bg-gradient-to-r from-slate-800 to-slate-900 hover:from-slate-700 hover:to-slate-800 text-white font-bold px-4 py-1 rounded-full shadow-lg shadow-slate-900/20 transition-all duration-200 active:scale-[0.98] flex items-center justify-center gap-2 text-base">
Login
</RouterLink>
</div>
<main class="px-5 flex flex-col justify-center">
<h1
class="text-center item-center text-3xl md:text-4xl font-extrabold tracking-tight bg-gradient-to-r from-stone-800 to-stone-700 bg-clip-text text-transparent leading-tight py-8">
<span v-html="titleHtml"></span>
</h1>
<div>
<div v-for="(item, index) in eventDetails" :key="index" class="mb-2 flex items-center gap-2">
<Icon :name="item.icon" />
<span>{{ item.value }}</span>
</div>
</div>
<div class="border-t border-stone-100 pt-5 flex flex-col gap-5">
<div>
<label class=" block text-sm font-semibold text-stone-700 mb-1.5">
Name <span class="text-amber-600">*</span>
</label>
<input type="text" v-model="form.name" placeholder="e.g., John Doe"
class="w-full px-4 py-2.5 rounded-xl border border-stone-300 bg-stone-50/40 focus:bg-white transition">
<p v-if="errors.name" class="text-xs text-rose-600 mt-1">{{ errors.name }}</p>
</div>
<div>
<label class=" block text-sm font-semibold text-stone-700 mb-1.5">
Phone Number <span class="text-amber-600">*</span>
</label>
<input type="tel" v-model="form.phone" placeholder="e.g., 0123456789"
class="w-full px-4 py-2.5 rounded-xl border border-stone-300 bg-stone-50/40 focus:bg-white transition">
<p v-if="errors.phone" class="text-xs text-rose-600 mt-1">{{ errors.phone }}</p>
</div>
<div>
<label class=" block text-sm font-semibold text-stone-700 mb-1.5">
Booking Mode
</label>
<div class="flex gap-3 bg-stone-100/60 p-1 rounded-xl">
<button type="button" @click="form.bookingMode = 'table'"
:class="['flex-1 py-2.5 rounded-lg font-medium transition',
form.bookingMode === 'table' ? 'bg-stone-800 text-white shadow-md' : 'bg-white text-stone-700 border border-stone-200']">
🪑 Table (10 pax)
</button>
<button type="button" @click="form.bookingMode = 'pax'"
:class="['flex-1 py-2.5 rounded-lg font-medium transition',
form.bookingMode === 'pax' ? 'bg-stone-800 text-white shadow-md' : 'bg-white text-stone-700 border border-stone-200']">
👥 Person
</button>
</div>
</div>
<div>
<label class="block text-sm font-semibold text-stone-700 mb-2">
{{ form.bookingMode === 'pax' ? 'Number of people' : 'Number of tables (席位)' }}
</label>
<div class="flex items-center justify-between bg-stone-50/70 rounded-xl p-1 border border-stone-200">
<button @click="adjustQuantity(-1)"
class="w-10 h-10 rounded-full bg-stone-100 flex items-center justify-center text-xl font-medium hover:bg-stone-200 transition"
:disabled="form.quantity <= 1" :class="{ 'opacity-40 cursor-not-allowed': form.quantity <= 1 }"></button>
<span class="text-2xl font-bold text-stone-800 min-w-[60px] text-center">{{ form.quantity }}</span>
<button @click="adjustQuantity(1)"
class="w-10 h-10 rounded-full bg-stone-100 flex items-center justify-center text-xl font-medium hover:bg-stone-200 transition">+</button>
</div>
</div>
<div>
<label class=" block text-sm font-semibold text-stone-700 mb-1.5">
Ticket Category
</label>
<div class="grid grid-cols-2 gap-3">
<button @click="form.ticketType = 'vip'"
:class="['py-2.5 rounded-xl border font-medium transition',
form.ticketType === 'vip' ? 'bg-amber-800 text-white border-amber-800' : 'bg-white border-stone-300 text-stone-700 hover:bg-stone-50']">
VIP <span class="block text-xs">RM150 / pax</span>
</button>
<button @click="form.ticketType = 'supporter'"
:class="['py-2.5 rounded-xl border font-medium transition',
form.ticketType === 'supporter' ? 'bg-stone-800 text-white border-stone-800' : 'bg-white border-stone-300 text-stone-700 hover:bg-stone-50']">
🎟 Supporter <span class="block text-xs">RM60 / pax</span>
</button>
</div>
</div>
<div class="bg-gradient-to-r from-stone-100 to-stone-50 rounded-xl p-4 border border-stone-200">
<div class="flex justify-between items-center flex-wrap">
<span class="text-stone-600 font-medium">💰 Total Price</span>
<span class="text-3xl font-black text-stone-800">RM {{ totalFormatted }}</span>
</div>
</div>
<div>
<label class="block text-sm font-semibold text-stone-700 mb-1.5">Person In Charge</label>
<select v-model="selectedPersonInCharge"
class="w-full px-4 py-2.5 rounded-xl border border-stone-300 bg-stone-50/40 focus:bg-white cursor-pointer">
<option v-for="(pic, index) in personInCharge" :key="index" :value="pic.phone">{{ pic.name }}</option>
</select>
<p class="text-xs text-stone-400 mt-1">WhatsApp message will be sent to your selected PIC</p>
</div>
<button id="getTicketBtn"
class="w-full bg-gradient-to-r from-slate-800 to-slate-900 hover:from-slate-700 hover:to-slate-800 text-white font-bold py-3.5 rounded-2xl shadow-lg shadow-slate-900/20 transition-all duration-200 active:scale-[0.98] flex items-center justify-center gap-2 text-base"
@click="bookTicket">
Book Your Ticker Now
</button>
</div>
</main>
<footer class="mt-8 border-t border-stone-100 px-5 py-4 text-center">
<p class="text-sm text-stone-400">© 2026 DAP 60th Anniversary Committee.<br>All rights reserved.</p>
</footer>
</div>
</template>
<script lang="ts" setup>
const titleHtml = "DAP JOHOR 60th<br>Anniversary Celebration";
const eventDetails = [
{
icon: "lucide:calendar",
value: "Saturday, 30th May 2026"
},
{
icon: "lucide:clock",
value: "6:30 PM"
},
{
icon: "lucide:map-pin",
value: "Yong Peng's Chee Ann Kor"
}
]
const personInCharge = [{
name: "Xiaomai",
phone: "601157753558"
}, {
name: "Lily",
phone: "60172661198"
}]
const form = ref({ name: '', phone: '', bookingMode: 'table', quantity: 1, ticketType: 'vip' })
const selectedPersonInCharge = ref(personInCharge[0]?.phone)
const errors = ref({ name: '', phone: '' })
const adjustQuantity = (delta: number) => {
const newQuantity = form.value.quantity + delta
if (newQuantity >= 1) {
form.value.quantity = newQuantity
}
}
const totalFormatted = computed(() => {
const price = form.value.ticketType === 'vip' ? 150 : 60
const total = form.value.quantity * (form.value.bookingMode === 'table' ? 10 : 1) * price
return total.toLocaleString('en-MY')
})
const generateBookingUrl = () => {
// Generate a base64 encoded string for the booking details
// The string should be in the format of JSON
// Link /booking?data={{base64EncodedString}}
const encodedPhone = btoa(form.value.phone)
return `${window.location.origin}/booking?data=${encodedPhone}`
}
const bookTicket = () => {
if (!form.value.name || !form.value.phone) {
errors.value.name = 'Name is required'
errors.value.phone = 'Phone number is required'
return
}
// https://wa.me/{{selectedPersonInCharge.value}}
// With text: I'd like to book a ticket for the DAP 60th Anniversary Celebration
const url = `https://wa.me/${selectedPersonInCharge.value}`
const text = `I'd like to book a ticket for the DAP 60th Anniversary Celebration%0A
Name: ${form.value.name}%0A
Phone Number: ${form.value.phone}%0A
Booking Mode: ${form.value.bookingMode}%0A
Quantity: ${form.value.quantity}%0A
Ticket Type: ${form.value.ticketType}%0A
Total Price: RM ${totalFormatted.value}%0A
Booking URL: ${generateBookingUrl()}`;
window.open(`${url}?text=${text}`, "_blank");
}
</script>
<style></style>