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:
@@ -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
1
app/assets/css/main.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
11
app/layouts/default.vue
Normal file
11
app/layouts/default.vue
Normal 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
184
app/pages/index/index.vue
Normal 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
13
app/pages/login/index.vue
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -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()]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|||||||
@@ -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
7
pnpm-lock.yaml
generated
@@ -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:
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user