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
This commit is contained in:
2026-04-12 17:53:01 +08:00
parent 25874073b1
commit 4288c98e21
9 changed files with 231 additions and 4 deletions

0
.codex Normal file
View File

View File

@@ -1,6 +1,7 @@
<template> <template>
<div> <div>
<NuxtRouteAnnouncer /> <NuxtLayout>
<NuxtWelcome /> <NuxtPage />
</NuxtLayout>
</div> </div>
</template> </template>

1
app/assets/css/main.css Normal file
View File

@@ -0,0 +1 @@
@import "tailwindcss";

11
app/layouts/default.vue Normal file
View File

@@ -0,0 +1,11 @@
<template>
<div>
<slot />
</div>
</template>
<script lang="ts" setup>
</script>
<style></style>

184
app/pages/index/index.vue Normal file
View File

@@ -0,0 +1,184 @@
<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>

13
app/pages/login/index.vue Normal file
View File

@@ -0,0 +1,13 @@
<template>
<div>
</div>
</template>
<script lang="ts" setup>
</script>
<style>
</style>

View File

@@ -1,6 +1,12 @@
import tailwindcss from '@tailwindcss/vite'
// https://nuxt.com/docs/api/configuration/nuxt-config // https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({ export default defineNuxtConfig({
compatibilityDate: '2025-07-15', compatibilityDate: '2025-07-15',
devtools: { enabled: true }, devtools: { enabled: true },
modules: ['@nuxt/ui'] modules: ['@nuxt/ui'],
css: ['~/assets/css/main.css'],
vite: {
plugins: [tailwindcss()]
}
}) })

View File

@@ -14,5 +14,9 @@
"nuxt": "^4.4.2", "nuxt": "^4.4.2",
"vue": "^3.5.32", "vue": "^3.5.32",
"vue-router": "^5.0.4" "vue-router": "^5.0.4"
},
"devDependencies": {
"@tailwindcss/vite": "^4.2.2",
"tailwindcss": "^4.2.2"
} }
} }

7
pnpm-lock.yaml generated
View File

@@ -20,6 +20,13 @@ importers:
vue-router: vue-router:
specifier: ^5.0.4 specifier: ^5.0.4
version: 5.0.4(@vue/compiler-sfc@3.5.32)(vue@3.5.32(typescript@6.0.2)) version: 5.0.4(@vue/compiler-sfc@3.5.32)(vue@3.5.32(typescript@6.0.2))
devDependencies:
'@tailwindcss/vite':
specifier: ^4.2.2
version: 4.2.2(vite@7.3.2(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3))
tailwindcss:
specifier: ^4.2.2
version: 4.2.2
packages: packages: